def setUp(self):
     self._mlist = create_list('*****@*****.**')
     self._msg = UserNotification(
         '*****@*****.**',
         '*****@*****.**',
         'Something you need to know',
         'I needed to tell you this.')
Beispiel #2
0
def _refuse(mlist, request, recip, comment, origmsg=None, lang=None):
    # As this message is going to the requester, try to set the language to
    # his/her language choice, if they are a member.  Otherwise use the list's
    # preferred language.
    display_name = mlist.display_name
    if lang is None:
        member = mlist.members.get_member(recip)
        lang = (mlist.preferred_language
                if member is None
                else member.preferred_language)
    text = make('refuse.txt',
                mailing_list=mlist,
                language=lang.code,
                listname=mlist.fqdn_listname,
                request=request,
                reason=comment,
                adminaddr=mlist.owner_address,
                )
    with _.using(lang.code):
        # add in original message, but not wrap/filled
        if origmsg:
            text = NL.join(
                [text,
                 '---------- ' + _('Original Message') + ' ----------',
                 str(origmsg)
                 ])
        subject = _('Request to mailing list "$display_name" rejected')
    msg = UserNotification(recip, mlist.bounces_address, subject, text, lang)
    msg.send(mlist)
Beispiel #3
0
def send_rejection(mlist, request, recip, comment, origmsg=None, lang=None):
    # As this message is going to the requester, try to set the language to
    # his/her language choice, if they are a member.  Otherwise use the list's
    # preferred language.
    display_name = mlist.display_name               # noqa
    if lang is None:
        member = mlist.members.get_member(recip)
        lang = (mlist.preferred_language
                if member is None
                else member.preferred_language)
    text = make('refuse.txt',
                mailing_list=mlist,
                language=lang.code,
                listname=mlist.fqdn_listname,
                request=request,
                reason=comment,
                adminaddr=mlist.owner_address,
                )
    with _.using(lang.code):
        # add in original message, but not wrap/filled
        if origmsg:
            text = NL.join(
                [text,
                 '---------- ' + _('Original Message') + ' ----------',
                 str(origmsg)
                 ])
        subject = _('Request to mailing list "$display_name" rejected')
    msg = UserNotification(recip, mlist.bounces_address, subject, text, lang)
    msg.send(mlist)
Beispiel #4
0
def hold_subscription(mlist, address, display_name, password, mode, language):
    data = dict(when=now().isoformat(),
                address=address,
                display_name=display_name,
                password=password,
                delivery_mode=mode.name,
                language=language)
    # Now hold this request.  We'll use the address as the key.
    requestsdb = IListRequests(mlist)
    request_id = requestsdb.hold_request(
        RequestType.subscription, address, data)
    vlog.info('%s: held subscription request from %s',
              mlist.fqdn_listname, address)
    # Possibly notify the administrator in default list language
    if mlist.admin_immed_notify:
        subject = _(
            'New subscription request to $mlist.display_name from $address')
        text = make('subauth.txt',
                    mailing_list=mlist,
                    username=address,
                    listname=mlist.fqdn_listname,
                    admindb_url=mlist.script_url('admindb'),
                    )
        # This message should appear to come from the <list>-owner so as
        # to avoid any useless bounce processing.
        msg = UserNotification(
            mlist.owner_address, mlist.owner_address,
            subject, text, mlist.preferred_language)
        msg.send(mlist, tomoderators=True)
    return request_id
Beispiel #5
0
def send_rejection(mlist, request, recip, comment, origmsg=None, lang=None):
    # As this message is going to the requester, try to set the language to
    # his/her language choice, if they are a member.  Otherwise use the list's
    # preferred language.
    display_name = mlist.display_name  # noqa: F841
    if lang is None:
        member = mlist.members.get_member(recip)
        lang = (mlist.preferred_language
                if member is None else member.preferred_language)
    template = getUtility(ITemplateLoader).get('list:user:notice:refuse',
                                               mlist)
    text = wrap(
        expand(
            template,
            mlist,
            dict(
                language=lang.code,
                reason=comment,
                # For backward compatibility.
                request=request,
                adminaddr=mlist.owner_address,
            )))
    with _.using(lang.code):
        # add in original message, but not wrap/filled
        if origmsg:
            text = NL.join([
                text, '---------- ' + _('Original Message') + ' ----------',
                str(origmsg)
            ])
        subject = _('Request to mailing list "$display_name" rejected')
    msg = UserNotification(recip, mlist.bounces_address, subject, text, lang)
    msg.send(mlist)
Beispiel #6
0
 def _step_get_moderator_approval(self):
     # Here's the next step in the workflow, assuming the moderator
     # approves of the subscription.  If they don't, the workflow and
     # subscription request will just be thrown away.
     self._set_token(TokenOwner.moderator)
     self.push('subscribe_from_restored')
     self.save()
     log.info('{}: held subscription request from {}'.format(
         self.mlist.fqdn_listname, self.address.email))
     # Possibly send a notification to the list moderators.
     if self.mlist.admin_immed_notify:
         subject = _(
             'New subscription request to $self.mlist.display_name '
             'from $self.address.email')
         username = formataddr(
             (self.subscriber.display_name, self.address.email))
         text = make('subauth.txt',
                     mailing_list=self.mlist,
                     username=username,
                     listname=self.mlist.fqdn_listname,
                     )
         # This message should appear to come from the <list>-owner so as
         # to avoid any useless bounce processing.
         msg = UserNotification(
             self.mlist.owner_address, self.mlist.owner_address,
             subject, text, self.mlist.preferred_language)
         msg.send(self.mlist, tomoderators=True)
     # The workflow must stop running here.
     raise StopIteration
Beispiel #7
0
def hold_subscription(mlist, address, display_name, password, mode, language):
    data = dict(when=now().isoformat(),
                address=address,
                display_name=display_name,
                password=password,
                delivery_mode=mode.name,
                language=language)
    # Now hold this request.  We'll use the address as the key.
    requestsdb = IListRequests(mlist)
    request_id = requestsdb.hold_request(RequestType.subscription, address,
                                         data)
    vlog.info('%s: held subscription request from %s', mlist.fqdn_listname,
              address)
    # Possibly notify the administrator in default list language
    if mlist.admin_immed_notify:
        subject = _(
            'New subscription request to $mlist.display_name from $address')
        text = make(
            'subauth.txt',
            mailing_list=mlist,
            username=address,
            listname=mlist.fqdn_listname,
            admindb_url=mlist.script_url('admindb'),
        )
        # This message should appear to come from the <list>-owner so as
        # to avoid any useless bounce processing.
        msg = UserNotification(mlist.owner_address, mlist.owner_address,
                               subject, text, mlist.preferred_language)
        msg.send(mlist, tomoderators=True)
    return request_id
class TestMessage(unittest.TestCase):
    """Test the message API."""

    layer = ConfigLayer

    def setUp(self):
        self._mlist = create_list('*****@*****.**')
        self._msg = UserNotification(
            '*****@*****.**',
            '*****@*****.**',
            'Something you need to know',
            'I needed to tell you this.')

    def test_one_precedence_header(self):
        # Ensure that when the original message already has a Precedence:
        # header, UserNotification.send(..., add_precedence=True, ...) does
        # not add a second header.
        self.assertEqual(self._msg['precedence'], None)
        self._msg['Precedence'] = 'omg wtf bbq'
        self._msg.send(self._mlist)
        items = get_queue_messages('virgin', expected_count=1)
        self.assertEqual(items[0].msg.get_all('precedence'),
                         ['omg wtf bbq'])

    def test_reduced_rfc_2369_headers(self):
        # Notifications should get reduced List-* headers.
        self._msg.send(self._mlist)
        items = get_queue_messages('virgin', expected_count=1)
        self.assertTrue(items[0].msgdata.get('reduced_list_headers'))
Beispiel #9
0
def bounce_message(mlist, msg, error=None):
    """Bounce the message back to the original author.

    :param mlist: The mailing list that the message was posted to.
    :type mlist: `IMailingList`
    :param msg: The original message.
    :type msg: `email.message.Message`
    :param error: Optional exception causing the bounce.  The exception
        instance must have a `.message` attribute.
    :type error: Exception
    """
    # Bounce a message back to the sender, with an error message if provided
    # in the exception argument.  .sender might be None or the empty string.
    if not msg.sender:
        # We can't bounce the message if we don't know who it's supposed to go
        # to.
        return
    subject = msg.get('subject', _('(no subject)'))
    subject = oneline(subject, mlist.preferred_language.charset)
    if error is None:
        notice = _('[No bounce details are available]')
    else:
        notice = _(error.message)
    # Currently we always craft bounces as MIME messages.
    bmsg = UserNotification(msg.sender, mlist.owner_address, subject,
                            lang=mlist.preferred_language)
    # BAW: Be sure you set the type before trying to attach, or you'll get
    # a MultipartConversionError.
    bmsg.set_type('multipart/mixed')
    txt = MIMEText(notice, _charset=mlist.preferred_language.charset)
    bmsg.attach(txt)
    bmsg.attach(MIMEMessage(msg))
    bmsg.send(mlist)
Beispiel #10
0
 def _step_get_moderator_approval(self):
     # Here's the next step in the workflow, assuming the moderator
     # approves of the subscription.  If they don't, the workflow and
     # subscription request will just be thrown away.
     self._set_token(TokenOwner.moderator)
     self.push('subscribe_from_restored')
     self.save()
     log.info('{}: held subscription request from {}'.format(
         self.mlist.fqdn_listname, self.address.email))
     # Possibly send a notification to the list moderators.
     if self.mlist.admin_immed_notify:
         subject = _(
             'New subscription request to $self.mlist.display_name '
             'from $self.address.email')
         username = formataddr(
             (self.subscriber.display_name, self.address.email))
         text = make('subauth.txt',
                     mailing_list=self.mlist,
                     username=username,
                     listname=self.mlist.fqdn_listname,
                     )
         # This message should appear to come from the <list>-owner so as
         # to avoid any useless bounce processing.
         msg = UserNotification(
             self.mlist.owner_address, self.mlist.owner_address,
             subject, text, self.mlist.preferred_language)
         msg.send(self.mlist, tomoderators=True)
     # The workflow must stop running here.
     raise StopIteration
Beispiel #11
0
def hold_unsubscription(mlist, email):
    data = dict(email=email)
    requestsdb = IListRequests(mlist)
    request_id = requestsdb.hold_request(RequestType.unsubscription, email,
                                         data)
    vlog.info('%s: held unsubscription request from %s', mlist.fqdn_listname,
              email)
    # Possibly notify the administrator of the hold
    if mlist.admin_immed_notify:
        subject = _(
            'New unsubscription request from $mlist.display_name by $email')
        template = getUtility(ITemplateLoader).get(
            'list:admin:action:unsubscribe', mlist)
        text = wrap(
            expand(
                template,
                mlist,
                dict(
                    # For backward compatibility.
                    mailing_list=mlist,
                    member=email,
                    email=email,
                )))
        # This message should appear to come from the <list>-owner so as
        # to avoid any useless bounce processing.
        msg = UserNotification(mlist.owner_address, mlist.owner_address,
                               subject, text, mlist.preferred_language)
        msg.send(mlist)
    return request_id
Beispiel #12
0
 def register(self, mlist, email, display_name=None, delivery_mode=None):
     """See `IUserRegistrar`."""
     if delivery_mode is None:
         delivery_mode = DeliveryMode.regular
     # First, do validation on the email address.  If the address is
     # invalid, it will raise an exception, otherwise it just returns.
     getUtility(IEmailValidator).validate(email)
     # Create a pendable for the registration.
     pendable = PendableRegistration(
         type=PendableRegistration.PEND_KEY, email=email, display_name=display_name, delivery_mode=delivery_mode.name
     )
     pendable["list_name"] = mlist.fqdn_listname
     token = getUtility(IPendings).add(pendable)
     # There are three ways for a user to confirm their subscription.  They
     # can reply to the original message and let the VERP'd return address
     # encode the token, they can reply to the robot and keep the token in
     # the Subject header, or they can click on the URL in the body of the
     # message and confirm through the web.
     subject = "confirm " + token
     confirm_address = mlist.confirm_address(token)
     # For i18n interpolation.
     confirm_url = mlist.domain.confirm_url(token)
     email_address = email
     domain_name = mlist.domain.mail_host
     contact_address = mlist.domain.contact_address
     # Send a verification email to the address.
     template = getUtility(ITemplateLoader).get(
         "mailman:///{0}/{1}/confirm.txt".format(mlist.fqdn_listname, mlist.preferred_language.code)
     )
     text = _(template)
     msg = UserNotification(email, confirm_address, subject, text)
     msg.send(mlist)
     return token
Beispiel #13
0
def autorespond_to_sender(mlist, sender, language=None):
    """Should Mailman automatically respond to this sender?

    :param mlist: The mailing list.
    :type mlist: `IMailingList`.
    :param sender: The sender's email address.
    :type sender: string
    :param language: Optional language.
    :type language: `ILanguage` or None
    :return: True if an automatic response should be sent, otherwise False.
        If an automatic response is not sent, a message is sent indicating
        that, er no more will be sent today.
    :rtype: bool
    """
    if language is None:
        language = mlist.preferred_language
    max_autoresponses_per_day = int(config.mta.max_autoresponses_per_day)
    if max_autoresponses_per_day == 0:
        # Unlimited.
        return True
    # Get an IAddress from an email address.
    user_manager = getUtility(IUserManager)
    address = user_manager.get_address(sender)
    if address is None:
        address = user_manager.create_address(sender)
    response_set = IAutoResponseSet(mlist)
    todays_count = response_set.todays_count(address, Response.hold)
    if todays_count < max_autoresponses_per_day:
        # This person has not reached their automatic response limit, so it's
        # okay to send a response.
        response_set.response_sent(address, Response.hold)
        return True
    elif todays_count == max_autoresponses_per_day:
        # The last one we sent was the last one we should send today.  Instead
        # of sending an automatic response, send them the "no more today"
        # message.
        log.info('hold autoresponse limit hit: %s', sender)
        response_set.response_sent(address, Response.hold)
        # Send this notification message instead.
        text = make(
            'nomoretoday.txt',
            language=language.code,
            sender=sender,
            listname=mlist.fqdn_listname,
            count=todays_count,
            owneremail=mlist.owner_address,
        )
        with _.using(language.code):
            msg = UserNotification(
                sender,
                mlist.owner_address,
                _('Last autoresponse notification for today'),
                text,
                lang=language)
        msg.send(mlist)
        return False
    else:
        # We've sent them everything we're going to send them today.
        log.info('Automatic response limit discard: %s', sender)
        return False
 def test_user_notification_bad_charset(self):
     msg = UserNotification(
         '*****@*****.**',
         '*****@*****.**',
         'Something you need to know',
         'Non-ascii text é.')
     self.assertEqual(msg.get_payload(), 'Non-ascii text ?.')
Beispiel #15
0
def send_welcome_message(mlist, member, language, text=''):
    """Send a welcome message to a subscriber.

    Prepending to the standard welcome message template is the mailing list's
    welcome message, if there is one.

    :param mlist: The mailing list.
    :type mlist: IMailingList
    :param member: The member to send the welcome message to.
    :param address: IMember
    :param language: The language of the response.
    :type language: ILanguage
    """
    welcome_message = _get_message(mlist.welcome_message_uri, mlist, language)
    options_url = member.options_url
    # Get the text from the template.
    display_name = ('' if member.user is None else member.user.display_name)
    text = expand(welcome_message, dict(
        fqdn_listname=mlist.fqdn_listname,
        list_name=mlist.display_name,
        listinfo_uri=mlist.script_url('listinfo'),
        list_requests=mlist.request_address,
        user_name=display_name,
        user_address=member.address.email,
        user_options_uri=options_url,
        ))
    digmode = ('' if member.delivery_mode is DeliveryMode.regular
               else _(' (Digest mode)'))
    msg = UserNotification(
        formataddr((display_name, member.address.email)),
        mlist.request_address,
        _('Welcome to the "$mlist.display_name" mailing list${digmode}'),
        text, language)
    msg['X-No-Archive'] = 'yes'
    msg.send(mlist, verp=as_boolean(config.mta.verp_personalized_deliveries))
Beispiel #16
0
def create(ctx, language, owners, notify, quiet, create_domain, fqdn_listname):
    language_code = (language if language is not None else
                     system_preferences.preferred_language.code)
    # Make sure that the selected language code is known.
    if language_code not in getUtility(ILanguageManager).codes:
        ctx.fail(_('Invalid language code: $language_code'))
    # Check to see if the domain exists or not.
    listname, at, domain = fqdn_listname.partition('@')
    domain_manager = getUtility(IDomainManager)
    if domain_manager.get(domain) is None and create_domain:
        domain_manager.add(domain)
    # Validate the owner email addresses.  The problem with doing this check in
    # create_list() is that you wouldn't be able to distinguish between an
    # InvalidEmailAddressError for the list name or the owners.  I suppose we
    # could subclass that exception though.
    if len(owners) > 0:
        validator = getUtility(IEmailValidator)
        invalid_owners = [
            owner for owner in owners if not validator.is_valid(owner)
        ]
        if invalid_owners:
            invalid = COMMASPACE.join(sorted(invalid_owners))  # noqa: F841
            ctx.fail(_('Illegal owner addresses: $invalid'))
    try:
        mlist = create_list(fqdn_listname, owners)
    except InvalidEmailAddressError:
        ctx.fail(_('Illegal list name: $fqdn_listname'))
    except ListAlreadyExistsError:
        ctx.fail(_('List already exists: $fqdn_listname'))
    except BadDomainSpecificationError as domain:  # noqa: F841
        ctx.fail(_('Undefined domain: $domain'))
    # Find the language associated with the code, then set the mailing list's
    # preferred language to that.
    language_manager = getUtility(ILanguageManager)
    with transaction():
        mlist.preferred_language = language_manager[language_code]
    # Do the notification.
    if not quiet:
        print(_('Created mailing list: $mlist.fqdn_listname'))
    if notify:
        template = getUtility(ITemplateLoader).get(
            'domain:admin:notice:new-list', mlist)
        text = wrap(
            expand(
                template,
                mlist,
                dict(
                    # For backward compatibility.
                    requestaddr=mlist.request_address,
                    siteowner=mlist.no_reply_address,
                )))
        # Set the I18N language to the list's preferred language so the header
        # will match the template language.  Stashing and restoring the old
        # translation context is just (healthy? :) paranoia.
        with _.using(mlist.preferred_language.code):
            msg = UserNotification(owners, mlist.no_reply_address,
                                   _('Your new mailing list: $fqdn_listname'),
                                   text, mlist.preferred_language)
            msg.send(mlist)
Beispiel #17
0
def send_probe(member, msg):
    """Send a VERP probe to the member.

    :param member: The member to send the probe to.  From this object, both
        the user and the mailing list can be determined.
    :type member: IMember
    :param msg: The bouncing message that caused the probe to be sent.
    :type msg:
    :return: The token representing this probe in the pendings database.
    :rtype: string
    """
    mlist = getUtility(IListManager).get_by_list_id(
        member.mailing_list.list_id)
    text = make(
        'probe.txt',
        mlist,
        member.preferred_language.code,
        listname=mlist.fqdn_listname,
        address=member.address.email,
        optionsurl=member.options_url,
        owneraddr=mlist.owner_address,
    )
    message_id = msg['message-id']
    if isinstance(message_id, bytes):
        message_id = message_id.decode('ascii')
    pendable = _ProbePendable(
        # We can only pend unicodes.
        member_id=member.member_id.hex,
        message_id=message_id,
    )
    token = getUtility(IPendings).add(pendable)
    mailbox, domain_parts = split_email(mlist.bounces_address)
    probe_sender = Template(config.mta.verp_probe_format).safe_substitute(
        bounces=mailbox,
        token=token,
        domain=DOT.join(domain_parts),
    )
    # Calculate the Subject header, in the member's preferred language.
    with _.using(member.preferred_language.code):
        subject = _('$mlist.display_name mailing list probe message')
    # Craft the probe message.  This will be a multipart where the first part
    # is the probe text and the second part is the message that caused this
    # probe to be sent.
    probe = UserNotification(member.address.email,
                             probe_sender,
                             subject,
                             lang=member.preferred_language)
    probe.set_type('multipart/mixed')
    notice = MIMEText(text, _charset=mlist.preferred_language.charset)
    probe.attach(notice)
    probe.attach(MIMEMessage(msg))
    # Probes should not have the Precedence: bulk header.
    probe.send(mlist,
               envsender=probe_sender,
               verp=False,
               probe_token=token,
               add_precedence=False)
    return token
Beispiel #18
0
def autorespond_to_sender(mlist, sender, language=None):
    """Should Mailman automatically respond to this sender?

    :param mlist: The mailing list.
    :type mlist: `IMailingList`.
    :param sender: The sender's email address.
    :type sender: string
    :param language: Optional language.
    :type language: `ILanguage` or None
    :return: True if an automatic response should be sent, otherwise False.
        If an automatic response is not sent, a message is sent indicating
        that, er no more will be sent today.
    :rtype: bool
    """
    if language is None:
        language = mlist.preferred_language
    max_autoresponses_per_day = int(config.mta.max_autoresponses_per_day)
    if max_autoresponses_per_day == 0:
        # Unlimited.
        return True
    # Get an IAddress from an email address.
    user_manager = getUtility(IUserManager)
    address = user_manager.get_address(sender)
    if address is None:
        address = user_manager.create_address(sender)
    response_set = IAutoResponseSet(mlist)
    todays_count = response_set.todays_count(address, Response.hold)
    if todays_count < max_autoresponses_per_day:
        # This person has not reached their automatic response limit, so it's
        # okay to send a response.
        response_set.response_sent(address, Response.hold)
        return True
    elif todays_count == max_autoresponses_per_day:
        # The last one we sent was the last one we should send today.  Instead
        # of sending an automatic response, send them the "no more today"
        # message.
        log.info('hold autoresponse limit hit: %s', sender)
        response_set.response_sent(address, Response.hold)
        # Send this notification message instead.
        text = make('nomoretoday.txt',
                    language=language.code,
                    sender=sender,
                    listname=mlist.fqdn_listname,
                    count=todays_count,
                    owneremail=mlist.owner_address,
                    )
        with _.using(language.code):
            msg = UserNotification(
                sender, mlist.owner_address,
                _('Last autoresponse notification for today'),
                text, lang=language)
        msg.send(mlist)
        return False
    else:
        # We've sent them everything we're going to send them today.
        log.info('Automatic response limit discard: %s', sender)
        return False
Beispiel #19
0
def send_welcome_message(mlist, address, language, delivery_mode, text=''):
    """Send a welcome message to a subscriber.

    Prepending to the standard welcome message template is the mailing list's
    welcome message, if there is one.

    :param mlist: the mailing list
    :type mlist: IMailingList
    :param address: The address to respond to
    :type address: string
    :param language: the language of the response
    :type language: ILanguage
    :param delivery_mode: the type of delivery the subscriber is getting
    :type delivery_mode: DeliveryMode
    """
    if mlist.welcome_message_uri:
        try:
            uri = expand(mlist.welcome_message_uri, dict(
                listname=mlist.fqdn_listname,
                language=language.code,
                ))
            welcome_message = getUtility(ITemplateLoader).get(uri)
        except URLError:
            log.exception('Welcome message URI not found ({0}): {1}'.format(
                mlist.fqdn_listname, mlist.welcome_message_uri))
            welcome = ''
        else:
            welcome = wrap(welcome_message)
    else:
        welcome = ''
    # Find the IMember object which is subscribed to the mailing list, because
    # from there, we can get the member's options url.
    member = mlist.members.get_member(address)
    user_name = member.user.display_name
    options_url = member.options_url
    # Get the text from the template.
    text = expand(welcome, dict(
        fqdn_listname=mlist.fqdn_listname,
        list_name=mlist.display_name,
        listinfo_uri=mlist.script_url('listinfo'),
        list_requests=mlist.request_address,
        user_name=user_name,
        user_address=address,
        user_options_uri=options_url,
        ))
    if delivery_mode is not DeliveryMode.regular:
        digmode = _(' (Digest mode)')
    else:
        digmode = ''
    msg = UserNotification(
        formataddr((user_name, address)),
        mlist.request_address,
        _('Welcome to the "$mlist.display_name" mailing list${digmode}'),
        text, language)
    msg['X-No-Archive'] = 'yes'
    msg.send(mlist, verp=as_boolean(config.mta.verp_personalized_deliveries))
Beispiel #20
0
def bounce_message(mlist, msg, error=None):
    """Bounce the message back to the original author.

    :param mlist: The mailing list that the message was posted to.
    :type mlist: `IMailingList`
    :param msg: The original message.
    :type msg: `email.message.Message`
    :param error: Optional exception causing the bounce.  The exception
        instance must have a `.message` attribute.
    :type error: Exception
    """
    # Bounce a message back to the sender, with an error message if provided
    # in the exception argument.  .sender might be None or the empty string.
    if not msg.sender:
        # We can't bounce the message if we don't know who it's supposed to go
        # to.
        return
    subject = msg.get('subject', _('(no subject)'))
    subject = oneline(subject, mlist.preferred_language.charset)
    if error is None:
        notice = _('[No bounce details are available]')
    else:
        notice = _(error.message)
    # Currently we always craft bounces as MIME messages.
    bmsg = UserNotification(msg.sender, mlist.owner_address, subject,
                            lang=mlist.preferred_language)
    # BAW: Be sure you set the type before trying to attach, or you'll get
    # a MultipartConversionError.
    bmsg.set_type('multipart/mixed')
    txt = MIMEText(notice, _charset=mlist.preferred_language.charset)
    bmsg.attach(txt)
    bmsg.attach(MIMEMessage(msg))
    bmsg.send(mlist)
Beispiel #21
0
def main():
    opts, args, parser = parseargs()
    initialize(opts.config)

    for name in config.list_manager.names:
        # The list must be locked in order to open the requests database
        mlist = MailList.MailList(name)
        try:
            count = IListRequests(mlist).count
            # While we're at it, let's evict yesterday's autoresponse data
            midnight_today = midnight()
            evictions = []
            for sender in mlist.hold_and_cmd_autoresponses.keys():
                date, respcount = mlist.hold_and_cmd_autoresponses[sender]
                if midnight(date) < midnight_today:
                    evictions.append(sender)
            if evictions:
                for sender in evictions:
                    del mlist.hold_and_cmd_autoresponses[sender]
                # This is the only place we've changed the list's database
                mlist.Save()
            if count:
                # Set the default language the the list's preferred language.
                _.default = mlist.preferred_language
                realname = mlist.real_name
                discarded = auto_discard(mlist)
                if discarded:
                    count = count - discarded
                    text = _('Notice: $discarded old request(s) '
                             'automatically expired.\n\n')
                else:
                    text = ''
                if count:
                    text += Utils.maketext(
                        'checkdbs.txt',
                        {'count'    : count,
                         'mail_host': mlist.mail_host,
                         'adminDB'  : mlist.GetScriptURL('admindb',
                                                         absolute=1),
                         'real_name': realname,
                         }, mlist=mlist)
                    text += '\n' + pending_requests(mlist)
                    subject = _('$count $realname moderator '
                                'request(s) waiting')
                else:
                    subject = _('$realname moderator request check result')
                msg = UserNotification(mlist.GetOwnerEmail(),
                                       mlist.GetBouncesEmail(),
                                       subject, text,
                                       mlist.preferred_language)
                msg.send(mlist, **{'tomoderators': True})
        finally:
            mlist.Unlock()
Beispiel #22
0
def main():
    opts, args, parser = parseargs()
    initialize(opts.config)

    for name in config.list_manager.names:
        # The list must be locked in order to open the requests database
        mlist = MailList.MailList(name)
        try:
            count = IListRequests(mlist).count
            # While we're at it, let's evict yesterday's autoresponse data
            midnight_today = midnight()
            evictions = []
            for sender in mlist.hold_and_cmd_autoresponses.keys():
                date, respcount = mlist.hold_and_cmd_autoresponses[sender]
                if midnight(date) < midnight_today:
                    evictions.append(sender)
            if evictions:
                for sender in evictions:
                    del mlist.hold_and_cmd_autoresponses[sender]
                # This is the only place we've changed the list's database
                mlist.Save()
            if count:
                # Set the default language the the list's preferred language.
                _.default = mlist.preferred_language
                realname = mlist.real_name
                discarded = auto_discard(mlist)
                if discarded:
                    count = count - discarded
                    text = _('Notice: $discarded old request(s) '
                             'automatically expired.\n\n')
                else:
                    text = ''
                if count:
                    text += Utils.maketext(
                        'checkdbs.txt', {
                            'count': count,
                            'mail_host': mlist.mail_host,
                            'adminDB': mlist.GetScriptURL('admindb',
                                                          absolute=1),
                            'real_name': realname,
                        },
                        mlist=mlist)
                    text += '\n' + pending_requests(mlist)
                    subject = _('$count $realname moderator '
                                'request(s) waiting')
                else:
                    subject = _('$realname moderator request check result')
                msg = UserNotification(mlist.GetOwnerEmail(),
                                       mlist.GetBouncesEmail(), subject, text,
                                       mlist.preferred_language)
                msg.send(mlist, **{'tomoderators': True})
        finally:
            mlist.Unlock()
Beispiel #23
0
def send_welcome_message(mlist, member, language, text=''):
    """Send a welcome message to a subscriber.

    Prepending to the standard welcome message template is the mailing list's
    welcome message, if there is one.

    :param mlist: The mailing list.
    :type mlist: IMailingList
    :param member: The member to send the welcome message to.
    :param address: IMember
    :param language: The language of the response.
    :type language: ILanguage
    """
    welcome_message = _get_message(mlist.welcome_message_uri, mlist, language)
    options_url = member.options_url
    # Try to find a non-empty display name.  We first look at the directly
    # subscribed record, which will either be the address or the user.  That's
    # handled automatically by going through member.subscriber.  If that
    # doesn't give us something useful, try whatever user is linked to the
    # subscriber.
    if member.subscriber.display_name:
        display_name = member.subscriber.display_name
    # If an unlinked address is subscribed tehre will be no .user.
    elif member.user is not None and member.user.display_name:
        display_name = member.user.display_name
    else:
        display_name = ''
    # Get the text from the template.
    text = expand(
        welcome_message,
        dict(
            fqdn_listname=mlist.fqdn_listname,
            list_name=mlist.display_name,
            listinfo_uri=mlist.script_url('listinfo'),
            list_requests=mlist.request_address,
            user_name=display_name,
            user_address=member.address.email,
            user_options_uri=options_url,
        ))
    digmode = (
        ''  # noqa
        if member.delivery_mode is DeliveryMode.regular else
        _(' (Digest mode)'))
    msg = UserNotification(
        formataddr(
            (display_name, member.address.email)), mlist.request_address,
        _('Welcome to the "$mlist.display_name" mailing list${digmode}'), text,
        language)
    msg['X-No-Archive'] = 'yes'
    msg.send(mlist, verp=as_boolean(config.mta.verp_personalized_deliveries))
Beispiel #24
0
 def setUp(self):
     self._mlist = create_list('*****@*****.**')
     self._msg = UserNotification(
         '*****@*****.**',
         '*****@*****.**',
         'Something you need to know',
         'I needed to tell you this.')
Beispiel #25
0
def send_welcome_message(mlist, member, language, text=''):
    """Send a welcome message to a subscriber.

    Prepending to the standard welcome message template is the mailing list's
    welcome message, if there is one.

    :param mlist: The mailing list.
    :type mlist: IMailingList
    :param member: The member to send the welcome message to.
    :param address: IMember
    :param language: The language of the response.
    :type language: ILanguage
    """
    welcome_message = _get_message(mlist.welcome_message_uri, mlist, language)
    options_url = member.options_url
    # Try to find a non-empty display name.  We first look at the directly
    # subscribed record, which will either be the address or the user.  That's
    # handled automatically by going through member.subscriber.  If that
    # doesn't give us something useful, try whatever user is linked to the
    # subscriber.
    if member.subscriber.display_name:
        display_name = member.subscriber.display_name
    # If an unlinked address is subscribed tehre will be no .user.
    elif member.user is not None and member.user.display_name:
        display_name = member.user.display_name
    else:
        display_name = ''
    # Get the text from the template.
    text = expand(welcome_message, dict(
        fqdn_listname=mlist.fqdn_listname,
        list_name=mlist.display_name,
        listinfo_uri=mlist.script_url('listinfo'),
        list_requests=mlist.request_address,
        user_name=display_name,
        user_address=member.address.email,
        user_options_uri=options_url,
        ))
    digmode = (''                                   # noqa
               if member.delivery_mode is DeliveryMode.regular
               else _(' (Digest mode)'))
    msg = UserNotification(
        formataddr((display_name, member.address.email)),
        mlist.request_address,
        _('Welcome to the "$mlist.display_name" mailing list${digmode}'),
        text, language)
    msg['X-No-Archive'] = 'yes'
    msg.send(mlist, verp=as_boolean(config.mta.verp_personalized_deliveries))
Beispiel #26
0
def send_probe(member, msg):
    """Send a VERP probe to the member.

    :param member: The member to send the probe to.  From this object, both
        the user and the mailing list can be determined.
    :type member: IMember
    :param msg: The bouncing message that caused the probe to be sent.
    :type msg:
    :return: The token representing this probe in the pendings database.
    :rtype: string
    """
    mlist = getUtility(IListManager).get_by_list_id(
        member.mailing_list.list_id)
    text = make('probe.txt', mlist, member.preferred_language.code,
                listname=mlist.fqdn_listname,
                address=member.address.email,
                optionsurl=member.options_url,
                owneraddr=mlist.owner_address,
                )
    message_id = msg['message-id']
    if isinstance(message_id, bytes):
        message_id = message_id.decode('ascii')
    pendable = _ProbePendable(
        # We can only pend unicodes.
        member_id=member.member_id.hex,
        message_id=message_id,
        )
    token = getUtility(IPendings).add(pendable)
    mailbox, domain_parts = split_email(mlist.bounces_address)
    probe_sender = Template(config.mta.verp_probe_format).safe_substitute(
        bounces=mailbox,
        token=token,
        domain=DOT.join(domain_parts),
        )
    # Calculate the Subject header, in the member's preferred language.
    with _.using(member.preferred_language.code):
        subject = _('$mlist.display_name mailing list probe message')
    # Craft the probe message.  This will be a multipart where the first part
    # is the probe text and the second part is the message that caused this
    # probe to be sent.
    probe = UserNotification(member.address.email, probe_sender,
                             subject, lang=member.preferred_language)
    probe.set_type('multipart/mixed')
    notice = MIMEText(text, _charset=mlist.preferred_language.charset)
    probe.attach(notice)
    probe.attach(MIMEMessage(msg))
    # Probes should not have the Precedence: bulk header.
    probe.send(mlist, envsender=probe_sender, verp=False, probe_token=token,
               add_precedence=False)
    return token
Beispiel #27
0
def send_goodbye_message(mlist, address, language):
    """Send a goodbye message to a subscriber.

    Prepending to the standard goodbye message template is the mailing list's
    goodbye message, if there is one.

    :param mlist: the mailing list
    :type mlist: IMailingList
    :param address: The address to respond to
    :type address: string
    :param language: the language of the response
    :type language: string
    """
    goodbye_message = _get_message(mlist.goodbye_message_uri, mlist, language)
    msg = UserNotification(
        address, mlist.bounces_address,
        _('You have been unsubscribed from the $mlist.display_name '
          'mailing list'), goodbye_message, language)
    msg.send(mlist, verp=as_boolean(config.mta.verp_personalized_deliveries))
def send_welcome_message(mlist, member, language, text=''):
    """Send a welcome message to a subscriber.

    Prepending to the standard welcome message template is the mailing list's
    welcome message, if there is one.

    :param mlist: The mailing list.
    :type mlist: IMailingList
    :param member: The member to send the welcome message to.
    :param address: IMember
    :param language: The language of the response.
    :type language: ILanguage
    """
    welcome_message = wrap(
        getUtility(ITemplateLoader).get('list:user:notice:welcome',
                                        mlist,
                                        language=language.code))
    display_name = member.display_name
    # Get the text from the template.
    text = expand(
        welcome_message,
        mlist,
        dict(
            user_name=display_name,
            user_email=member.address.email,
            # For backward compatibility.
            user_address=member.address.email,
            fqdn_listname=mlist.fqdn_listname,
            list_name=mlist.display_name,
            list_requests=mlist.request_address,
        ))
    digmode = (
        ''  # noqa: F841
        if member.delivery_mode is DeliveryMode.regular else
        _(' (Digest mode)'))
    msg = UserNotification(
        formataddr(
            (display_name, member.address.email)), mlist.request_address,
        _('Welcome to the "$mlist.display_name" mailing list${digmode}'), text,
        language)
    msg['X-No-Archive'] = 'yes'
    msg.send(mlist, verp=as_boolean(config.mta.verp_personalized_deliveries))
Beispiel #29
0
def send_goodbye_message(mlist, address, language):
    """Send a goodbye message to a subscriber.

    Prepending to the standard goodbye message template is the mailing list's
    goodbye message, if there is one.

    :param mlist: the mailing list
    :type mlist: IMailingList
    :param address: The address to respond to
    :type address: string
    :param language: the language of the response
    :type language: string
    """
    goodbye_message = _get_message(mlist.goodbye_message_uri,
                                   mlist, language)
    msg = UserNotification(
        address, mlist.bounces_address,
        _('You have been unsubscribed from the $mlist.display_name '
          'mailing list'),
        goodbye_message, language)
    msg.send(mlist, verp=as_boolean(config.mta.verp_personalized_deliveries))
Beispiel #30
0
 def process(self, mlist, msg, msgdata):
     """See `IHandler`."""
     # Extract the sender's address and find them in the user database
     sender = msgdata.get('original_sender', msg.sender)
     member = mlist.members.get_member(sender)
     if member is None or not member.acknowledge_posts:
         # Either the sender is not a member, in which case we can't know
         # whether they want an acknowlegment or not, or they are a member
         # who definitely does not want an acknowlegment.
         return
     # Okay, they are a member that wants an acknowledgment of their post.
     # Give them their original subject.  BAW: do we want to use the
     # decoded header?
     original_subject = msgdata.get(
         'origsubj', msg.get('subject', _('(no subject)')))
     # Get the user's preferred language.
     language_manager = getUtility(ILanguageManager)
     language = (language_manager[msgdata['lang']]
                 if 'lang' in msgdata
                 else member.preferred_language)
     charset = language_manager[language.code].charset
     # Now get the acknowledgement template.
     display_name = mlist.display_name
     text = make('postack.txt',
                 mailing_list=mlist,
                 language=language.code,
                 wrap=False,
                 subject=oneline(original_subject, charset),
                 list_name=mlist.list_name,
                 display_name=display_name,
                 listinfo_url=mlist.script_url('listinfo'),
                 optionsurl=member.options_url,
                 )
     # Craft the outgoing message, with all headers and attributes
     # necessary for general delivery.  Then enqueue it to the outgoing
     # queue.
     subject = _('$display_name post acknowledgment')
     usermsg = UserNotification(sender, mlist.bounces_address,
                                subject, text, language)
     usermsg.send(mlist)
def send_user_disable_warning(mlist, address, language):
    """Sends a warning mail to the user reminding the person to
    reenable its DeliveryStatus.

    :param mlist: The mailing list
    :type mlist: IMailingList
    :param address: The address of the member
    :type address: string.
    :param language: member's preferred language
    :type language: ILanguage
    """
    warning_message = wrap(
        getUtility(ITemplateLoader).get('list:user:notice:warning',
                                        mlist,
                                        language=language.code))
    warning_message_text = expand(warning_message, mlist,
                                  dict(sender_email=address))
    msg = UserNotification(
        address, mlist.bounces_address,
        _('Your subscription for ${mlist.display_name} mailing list'
          ' has been disabled'), warning_message_text, language)
    msg.send(mlist, verp=as_boolean(config.mta.verp_personalized_deliveries))
Beispiel #32
0
def _handle_confirmation_needed_events(event, template_name):
    subject = 'confirm {}'.format(event.token)
    confirm_address = event.mlist.confirm_address(event.token)
    email_address = event.email
    # Send a verification email to the address.
    template = getUtility(ITemplateLoader).get(template_name, event.mlist)
    text = expand(
        template,
        event.mlist,
        dict(
            token=event.token,
            subject=subject,
            confirm_email=confirm_address,
            user_email=email_address,
            # For backward compatibility.
            confirm_address=confirm_address,
            email_address=email_address,
            domain_name=event.mlist.domain.mail_host,
            contact_address=event.mlist.owner_address,
        ))
    msg = UserNotification(email_address, confirm_address, subject, text)
    msg.send(event.mlist, add_precedence=False)
Beispiel #33
0
def handle_ConfirmationNeededEvent(event):
    if not isinstance(event, ConfirmationNeededEvent):
        return
    # There are three ways for a user to confirm their subscription.  They
    # can reply to the original message and let the VERP'd return address
    # encode the token, they can reply to the robot and keep the token in
    # the Subject header, or they can click on the URL in the body of the
    # message and confirm through the web.
    subject = 'confirm ' + event.token
    confirm_address = event.mlist.confirm_address(event.token)
    # For i18n interpolation.
    confirm_url = event.mlist.domain.confirm_url(event.token)  # noqa
    email_address = event.email
    domain_name = event.mlist.domain.mail_host  # noqa
    contact_address = event.mlist.owner_address  # noqa
    # Send a verification email to the address.
    template = getUtility(ITemplateLoader).get(
        'mailman:///{}/{}/confirm.txt'.format(
            event.mlist.fqdn_listname, event.mlist.preferred_language.code))
    text = _(template)
    msg = UserNotification(email_address, confirm_address, subject, text)
    msg.send(event.mlist, add_precedence=False)
Beispiel #34
0
 def process(self, mlist, msg, msgdata):
     """See `IHandler`."""
     # Extract the sender's address and find them in the user database
     sender = msgdata.get('original_sender', msg.sender)
     member = mlist.members.get_member(sender)
     if member is None or not member.acknowledge_posts:
         # Either the sender is not a member, in which case we can't know
         # whether they want an acknowlegment or not, or they are a member
         # who definitely does not want an acknowlegment.
         return
     # Okay, they are a member that wants an acknowledgment of their post.
     # Give them their original subject.  BAW: do we want to use the
     # decoded header?
     original_subject = msgdata.get('origsubj',
                                    msg.get('subject', _('(no subject)')))
     # Get the user's preferred language.
     language_manager = getUtility(ILanguageManager)
     language = (language_manager[msgdata['lang']]
                 if 'lang' in msgdata else member.preferred_language)
     # Now get the acknowledgement template.
     display_name = mlist.display_name  # noqa: F841
     template = getUtility(ITemplateLoader).get('list:user:notice:post',
                                                mlist,
                                                language=language.code)
     text = expand(
         template,
         mlist,
         dict(
             subject=oneline(original_subject, in_unicode=True),
             # For backward compatibility.
             list_name=mlist.list_name,
         ))
     # Craft the outgoing message, with all headers and attributes
     # necessary for general delivery.  Then enqueue it to the outgoing
     # queue.
     subject = _('$display_name post acknowledgment')
     usermsg = UserNotification(sender, mlist.bounces_address, subject,
                                text, language)
     usermsg.send(mlist)
Beispiel #35
0
class TestMessage(unittest.TestCase):
    """Test the message API."""

    layer = ConfigLayer

    def setUp(self):
        self._mlist = create_list('*****@*****.**')
        self._msg = UserNotification('*****@*****.**', '*****@*****.**',
                                     'Something you need to know',
                                     'I needed to tell you this.')

    def test_one_precedence_header(self):
        # Ensure that when the original message already has a Precedence:
        # header, UserNotification.send(..., add_precedence=True, ...) does
        # not add a second header.
        self.assertEqual(self._msg['precedence'], None)
        self._msg['Precedence'] = 'omg wtf bbq'
        self._msg.send(self._mlist)
        messages = get_queue_messages('virgin')
        self.assertEqual(len(messages), 1)
        self.assertEqual(messages[0].msg.get_all('precedence'),
                         ['omg wtf bbq'])
Beispiel #36
0
def handle_ConfirmationNeededEvent(event):
    if not isinstance(event, ConfirmationNeededEvent):
        return
    # There are three ways for a user to confirm their subscription.  They
    # can reply to the original message and let the VERP'd return address
    # encode the token, they can reply to the robot and keep the token in
    # the Subject header, or they can click on the URL in the body of the
    # message and confirm through the web.
    subject = 'confirm ' + event.token
    confirm_address = event.mlist.confirm_address(event.token)
    # For i18n interpolation.
    confirm_url = event.mlist.domain.confirm_url(event.token)
    email_address = event.email
    domain_name = event.mlist.domain.mail_host
    contact_address = event.mlist.owner_address
    # Send a verification email to the address.
    template = getUtility(ITemplateLoader).get(
        'mailman:///{0}/{1}/confirm.txt'.format(
            event.mlist.fqdn_listname,
            event.mlist.preferred_language.code))
    text = _(template)
    msg = UserNotification(email_address, confirm_address, subject, text)
    msg.send(event.mlist, add_precedence=False)
Beispiel #37
0
class TestMessage(unittest.TestCase):
    """Test the message API."""

    layer = ConfigLayer

    def setUp(self):
        self._mlist = create_list('*****@*****.**')
        self._msg = UserNotification(
            '*****@*****.**',
            '*****@*****.**',
            'Something you need to know',
            'I needed to tell you this.')

    def test_one_precedence_header(self):
        # Ensure that when the original message already has a Precedence:
        # header, UserNotification.send(..., add_precedence=True, ...) does
        # not add a second header.
        self.assertEqual(self._msg['precedence'], None)
        self._msg['Precedence'] = 'omg wtf bbq'
        self._msg.send(self._mlist)
        items = get_queue_messages('virgin', expected_count=1)
        self.assertEqual(items[0].msg.get_all('precedence'),
                         ['omg wtf bbq'])
Beispiel #38
0
 def _step_get_moderator_approval(self):
     self._set_token(TokenOwner.moderator)
     self.push('unsubscribe_from_restored')
     self.save()
     log.info('{}: held unsubscription request from {}'.format(
         self.mlist.fqdn_listname, self.address.email))
     if self.mlist.admin_immed_notify:
         subject = _(
             'New unsubscription request to $self.mlist.display_name '
             'from $self.address.email')
         username = formataddr(
             (self.subscriber.display_name, self.address.email))
         template = getUtility(ITemplateLoader).get(
             'list:admin:action:unsubscribe', self.mlist)
         text = wrap(expand(template, self.mlist, dict(member=username, )))
         # This message should appear to come from the <list>-owner so as
         # to avoid any useless bounce processing.
         msg = UserNotification(self.mlist.owner_address,
                                self.mlist.owner_address, subject, text,
                                self.mlist.preferred_language)
         msg.send(self.mlist)
     # The workflow must stop running here
     raise StopIteration
Beispiel #39
0
def send_welcome_message(mlist, member, language, text=''):
    """Send a welcome message to a subscriber.

    Prepending to the standard welcome message template is the mailing list's
    welcome message, if there is one.

    :param mlist: The mailing list.
    :type mlist: IMailingList
    :param member: The member to send the welcome message to.
    :param address: IMember
    :param language: The language of the response.
    :type language: ILanguage
    """
    welcome_message = _get_message(mlist.welcome_message_uri, mlist, language)
    options_url = member.options_url
    # Get the text from the template.
    display_name = ('' if member.user is None else member.user.display_name)
    text = expand(
        welcome_message,
        dict(
            fqdn_listname=mlist.fqdn_listname,
            list_name=mlist.display_name,
            listinfo_uri=mlist.script_url('listinfo'),
            list_requests=mlist.request_address,
            user_name=display_name,
            user_address=member.address.email,
            user_options_uri=options_url,
        ))
    digmode = ('' if member.delivery_mode is DeliveryMode.regular else
               _(' (Digest mode)'))
    msg = UserNotification(
        formataddr(
            (display_name, member.address.email)), mlist.request_address,
        _('Welcome to the "$mlist.display_name" mailing list${digmode}'), text,
        language)
    msg['X-No-Archive'] = 'yes'
    msg.send(mlist, verp=as_boolean(config.mta.verp_personalized_deliveries))
Beispiel #40
0
def _handle_confirmation_needed_events(event, template_name):
    # This function handles sending the confirmation email to the user
    # for both subscriptions requiring confirmation and invitations
    # requiring acceptance.
    if template_name.endswith(':invite'):
        subject = _('You have been invited to join the '
                    '$event.mlist.fqdn_listname mailing list.')
    elif template_name.endswith(':unsubscribe'):
        subject = _('Your confirmation is needed to leave the '
                    '$event.mlist.fqdn_listname mailing list.')
    else:
        assert (template_name.endswith(':subscribe'))
        subject = _('Your confirmation is needed to join the '
                    '$event.mlist.fqdn_listname mailing list.')
    confirm_address = event.mlist.confirm_address(event.token)
    email_address = event.email
    # Send a verification email to the address.
    template = getUtility(ITemplateLoader).get(template_name, event.mlist)
    text = expand(
        template,
        event.mlist,
        dict(
            token=event.token,
            subject=subject,
            confirm_email=confirm_address,
            user_email=email_address,
            # For backward compatibility.
            confirm_address=confirm_address,
            email_address=email_address,
            domain_name=event.mlist.domain.mail_host,
            contact_address=event.mlist.owner_address,
        ))
    msg = UserNotification(email_address, confirm_address, subject, text,
                           event.mlist.preferred_language)
    del msg['auto-submitted']
    msg['Auto-Submitted'] = 'auto-generated'
    msg.send(event.mlist, add_precedence=False)
Beispiel #41
0
def bounce_message(mlist, msg, error=None):
    """Bounce the message back to the original author.

    :param mlist: The mailing list that the message was posted to.
    :type mlist: `IMailingList`
    :param msg: The original message.
    :type msg: `email.message.Message`
    :param error: Optional exception causing the bounce.  The exception
        instance must have a `.message` attribute.  The exception *may* have a
        non-None `.reasons` attribute which would be a list of reasons for the
        rejection, and it may have a non-None `.substitutions` attribute.  The
        latter, along with the formatted reasons will be interpolated into the
        message (`.reasons` gets put into the `$reasons` placeholder).
    :type error: RejectMessage
    """
    # Bounce a message back to the sender, with an error message if provided
    # in the exception argument.  .sender might be None or the empty string.
    if not msg.sender:
        # We can't bounce the message if we don't know who it's supposed to go
        # to.
        return
    subject = msg.get('subject', _('(no subject)'))
    subject = oneline(subject, mlist.preferred_language.charset)
    notice = (_('[No bounce details are available]')
              if error is None else str(error))
    # Currently we always craft bounces as MIME messages.
    bmsg = UserNotification(msg.sender,
                            mlist.owner_address,
                            subject,
                            lang=mlist.preferred_language)
    # BAW: Be sure you set the type before trying to attach, or you'll get
    # a MultipartConversionError.
    bmsg.set_type('multipart/mixed')
    txt = MIMEText(notice, _charset=mlist.preferred_language.charset)
    bmsg.attach(txt)
    bmsg.attach(MIMEMessage(msg))
    bmsg.send(mlist)
Beispiel #42
0
def hold_unsubscription(mlist, email):
    data = dict(email=email)
    requestsdb = IListRequests(mlist)
    request_id = requestsdb.hold_request(
        RequestType.unsubscription, email, data)
    vlog.info('%s: held unsubscription request from %s',
              mlist.fqdn_listname, email)
    # Possibly notify the administrator of the hold
    if mlist.admin_immed_notify:
        subject = _(
            'New unsubscription request from $mlist.display_name by $email')
        text = make('unsubauth.txt',
                    mailing_list=mlist,
                    email=email,
                    listname=mlist.fqdn_listname,
                    admindb_url=mlist.script_url('admindb'),
                    )
        # This message should appear to come from the <list>-owner so as
        # to avoid any useless bounce processing.
        msg = UserNotification(
            mlist.owner_address, mlist.owner_address,
            subject, text, mlist.preferred_language)
        msg.send(mlist, tomoderators=True)
    return request_id
Beispiel #43
0
def hold_unsubscription(mlist, address):
    data = dict(address=address)
    requestsdb = IListRequests(mlist)
    request_id = requestsdb.hold_request(
        RequestType.unsubscription, address, data)
    vlog.info('%s: held unsubscription request from %s',
              mlist.fqdn_listname, address)
    # Possibly notify the administrator of the hold
    if mlist.admin_immed_notify:
        subject = _(
            'New unsubscription request from $mlist.display_name by $address')
        text = make('unsubauth.txt',
                    mailing_list=mlist,
                    address=address,
                    listname=mlist.fqdn_listname,
                    admindb_url=mlist.script_url('admindb'),
                    )
        # This message should appear to come from the <list>-owner so as
        # to avoid any useless bounce processing.
        msg = UserNotification(
            mlist.owner_address, mlist.owner_address,
            subject, text, mlist.preferred_language)
        msg.send(mlist, tomoderators=True)
    return request_id
Beispiel #44
0
    def _process(self, mlist, msg, msgdata):
        """See `TerminalChainBase`."""
        # Start by decorating the message with a header that contains a list
        # of all the rules that matched.  These metadata could be None or an
        # empty list.
        rule_hits = msgdata.get('rule_hits')
        if rule_hits:
            msg['X-Mailman-Rule-Hits'] = SEMISPACE.join(rule_hits)
        rule_misses = msgdata.get('rule_misses')
        if rule_misses:
            msg['X-Mailman-Rule-Misses'] = SEMISPACE.join(rule_misses)
        # Hold the message by adding it to the list's request database.
        request_id = hold_message(mlist, msg, msgdata, None)
        # Calculate a confirmation token to send to the author of the
        # message.
        pendable = HeldMessagePendable(type=HeldMessagePendable.PEND_KEY,
                                       id=request_id)
        token = getUtility(IPendings).add(pendable)
        # Get the language to send the response in.  If the sender is a
        # member, then send it in the member's language, otherwise send it in
        # the mailing list's preferred language.
        member = mlist.members.get_member(msg.sender)
        language = (member.preferred_language
                    if member else mlist.preferred_language)
        # A substitution dictionary for the email templates.
        charset = mlist.preferred_language.charset
        original_subject = msg.get('subject')
        if original_subject is None:
            original_subject = _('(no subject)')
        else:
            original_subject = oneline(original_subject, in_unicode=True)
        substitutions = dict(
            listname    = mlist.fqdn_listname,
            subject     = original_subject,
            sender      = msg.sender,
            reasons     = _compose_reasons(msgdata),
            )
        # At this point the message is held, but now we have to craft at least
        # two responses.  The first will go to the original author of the
        # message and it will contain the token allowing them to approve or
        # discard the message.  The second one will go to the moderators of
        # the mailing list, if the list is so configured.
        #
        # Start by possibly sending a response to the message author.  There
        # are several reasons why we might not go through with this.  If the
        # message was gated from NNTP, the author may not even know about this
        # list, so don't spam them.  If the author specifically requested that
        # acknowledgments not be sent, or if the message was bulk email, then
        # we do not send the response.  It's also possible that either the
        # mailing list, or the author (if they are a member) have been
        # configured to not send such responses.
        if (not msgdata.get('fromusenet') and
            can_acknowledge(msg) and
            mlist.respond_to_post_requests and
            autorespond_to_sender(mlist, msg.sender, language)):
            # We can respond to the sender with a message indicating their
            # posting was held.
            subject = _(
              'Your message to $mlist.fqdn_listname awaits moderator approval')
            send_language_code = msgdata.get('lang', language.code)
            text = make('postheld.txt',
                        mailing_list=mlist,
                        language=send_language_code,
                        **substitutions)
            adminaddr = mlist.bounces_address
            nmsg = UserNotification(
                msg.sender, adminaddr, subject, text,
                getUtility(ILanguageManager)[send_language_code])
            nmsg.send(mlist)
        # Now the message for the list moderators.  This one should appear to
        # come from <list>-owner since we really don't need to do bounce
        # processing on it.
        if mlist.admin_immed_notify:
            # Now let's temporarily set the language context to that which the
            # administrators are expecting.
            with _.using(mlist.preferred_language.code):
                language = mlist.preferred_language
                charset = language.charset
                substitutions['subject'] = original_subject
                # We need to regenerate or re-translate a few values in the
                # substitution dictionary.
                substitutions['reasons'] = _compose_reasons(msgdata, 55)
                # craft the admin notification message and deliver it
                subject = _(
                    '$mlist.fqdn_listname post from $msg.sender requires '
                    'approval')
                nmsg = UserNotification(mlist.owner_address,
                                        mlist.owner_address,
                                        subject, lang=language)
                nmsg.set_type('multipart/mixed')
                text = MIMEText(make('postauth.txt',
                                     mailing_list=mlist,
                                     wrap=False,
                                     **substitutions),
                                _charset=charset)
                dmsg = MIMEText(wrap(_("""\
If you reply to this message, keeping the Subject: header intact, Mailman will
discard the held message.  Do this if the message is spam.  If you reply to
this message and include an Approved: header with the list password in it, the
message will be approved for posting to the list.  The Approved: header can
also appear in the first line of the body of the reply.""")),
                                _charset=language.charset)
                dmsg['Subject'] = 'confirm ' + token
                dmsg['From'] = mlist.request_address
                dmsg['Date'] = formatdate(localtime=True)
                dmsg['Message-ID'] = make_msgid()
                nmsg.attach(text)
                nmsg.attach(MIMEMessage(msg))
                nmsg.attach(MIMEMessage(dmsg))
                nmsg.send(mlist, **dict(tomoderators=True))
        # Log the held message.  Log messages are not translated, so recast
        # the reasons in the English.
        with _.using('en'):
            reasons = _compose_reasons(msgdata)
            log.info('HOLD: %s post from %s held, message-id=%s: %s',
                     mlist.fqdn_listname, msg.sender,
                     msg.get('message-id', 'n/a'), SEMISPACE.join(reasons))
        notify(HoldEvent(mlist, msg, msgdata, self))
Beispiel #45
0
def handle_message(mlist, id, action,
                   comment=None, preserve=False, forward=None):
    message_store = getUtility(IMessageStore)
    requestdb = IListRequests(mlist)
    key, msgdata = requestdb.get_request(id)
    # Handle the action.
    rejection = None
    message_id = msgdata['_mod_message_id']
    sender = msgdata['_mod_sender']
    subject = msgdata['_mod_subject']
    if action in (Action.defer, Action.hold):
        # Nothing to do, but preserve the message for later.
        preserve = True
    elif action is Action.discard:
        rejection = 'Discarded'
    elif action is Action.reject:
        rejection = 'Refused'
        member = mlist.members.get_member(sender)
        if member:
            language = member.preferred_language
        else:
            language = None
        _refuse(mlist, _('Posting of your message titled "$subject"'),
                sender, comment or _('[No reason given]'), language)
    elif action is Action.accept:
        # Start by getting the message from the message store.
        msg = message_store.get_message_by_id(message_id)
        # Delete moderation-specific entries from the message metadata.
        for key in msgdata.keys():
            if key.startswith('_mod_'):
                del msgdata[key]
        # Add some metadata to indicate this message has now been approved.
        msgdata['approved'] = True
        msgdata['moderator_approved'] = True
        # Calculate a new filebase for the approved message, otherwise
        # delivery errors will cause duplicates.
        if 'filebase' in msgdata:
            del msgdata['filebase']
        # Queue the file for delivery.  Trying to deliver the message directly
        # here can lead to a huge delay in web turnaround.  Log the moderation
        # and add a header.
        msg['X-Mailman-Approved-At'] = formatdate(
            time.mktime(now().timetuple()), localtime=True)
        vlog.info('held message approved, message-id: %s',
                  msg.get('message-id', 'n/a'))
        # Stick the message back in the incoming queue for further
        # processing.
        config.switchboards['pipeline'].enqueue(msg, _metadata=msgdata)
    else:
        raise AssertionError('Unexpected action: {0}'.format(action))
    # Forward the message.
    if forward:
        # Get a copy of the original message from the message store.
        msg = message_store.get_message_by_id(message_id)
        # It's possible the forwarding address list is a comma separated list
        # of display_name/address pairs.
        addresses = [addr[1] for addr in getaddresses(forward)]
        language = mlist.preferred_language
        if len(addresses) == 1:
            # If the address getting the forwarded message is a member of
            # the list, we want the headers of the outer message to be
            # encoded in their language.  Otherwise it'll be the preferred
            # language of the mailing list.  This is better than sending a
            # separate message per recipient.
            member = mlist.members.get_member(addresses[0])
            if member:
                language = member.preferred_language
        with _.using(language.code):
            fmsg = UserNotification(
                addresses, mlist.bounces_address,
                _('Forward of moderated message'),
                lang=language)
        fmsg.set_type('message/rfc822')
        fmsg.attach(msg)
        fmsg.send(mlist)
    # Delete the message from the message store if it is not being preserved.
    if not preserve:
        message_store.delete_message(message_id)
        requestdb.delete_request(id)
    # Log the rejection
    if rejection:
        note = """%s: %s posting:
\tFrom: %s
\tSubject: %s"""
        if comment:
            note += '\n\tReason: ' + comment
        vlog.info(note, mlist.fqdn_listname, rejection, sender, subject)
Beispiel #46
0
 def process(self, mlist, msg, msgdata):
     """See `IHandler`."""
     # There are several cases where the replybot is short-circuited:
     # * the original message has an "X-Ack: No" header
     # * the message has a Precedence header with values bulk, junk, or
     #   list, and there's no explicit "X-Ack: yes" header
     # * the message metadata has a true 'noack' key
     ack = msg.get('x-ack', '').lower()
     if ack == 'no' or msgdata.get('noack'):
         return
     precedence = msg.get('precedence', '').lower()
     if ack != 'yes' and precedence in ('bulk', 'junk', 'list'):
         return
     # Check to see if the list is even configured to autorespond to this
     # email message.  Note: the incoming message processors should set the
     # destination key in the message data.
     if msgdata.get('to_owner'):
         if mlist.autorespond_owner is ResponseAction.none:
             return
         response_type = Response.owner
         response_text = mlist.autoresponse_owner_text
     elif msgdata.get('to_request'):
         if mlist.autorespond_requests is ResponseAction.none:
             return
         response_type = Response.command
         response_text = mlist.autoresponse_request_text
     elif msgdata.get('to_list'):
         if mlist.autorespond_postings is ResponseAction.none:
             return
         response_type = Response.postings
         response_text = mlist.autoresponse_postings_text
     else:
         # There are no automatic responses for any other destination.
         return
     # Now see if we're in the grace period for this sender.  grace_period
     # = 0 means always automatically respond, as does an "X-Ack: yes"
     # header (useful for debugging).
     response_set = IAutoResponseSet(mlist)
     user_manager = getUtility(IUserManager)
     address = user_manager.get_address(msg.sender)
     if address is None:
         address = user_manager.create_address(msg.sender)
     grace_period = mlist.autoresponse_grace_period
     if grace_period > ALWAYS_REPLY and ack != 'yes':
         last = response_set.last_response(address, response_type)
         if last is not None and last.date_sent + grace_period > today():
             return
     # Okay, we know we're going to respond to this sender, craft the
     # message, send it, and update the database.
     display_name = mlist.display_name
     subject = _(
         'Auto-response for your message to the "$display_name" '
         'mailing list')
     # Do string interpolation into the autoresponse text
     d = dict(list_name = mlist.list_name,
              display_name = display_name,
              listurl = mlist.script_url('listinfo'),
              requestemail = mlist.request_address,
              owneremail = mlist.owner_address,
              )
     # Interpolation and Wrap the response text.
     text = wrap(expand(response_text, d))
     outmsg = UserNotification(msg.sender, mlist.bounces_address,
                               subject, text, mlist.preferred_language)
     outmsg['X-Mailer'] = _('The Mailman Replybot')
     # prevent recursions and mail loops!
     outmsg['X-Ack'] = 'No'
     outmsg.send(mlist)
     response_set.response_sent(address, response_type)
Beispiel #47
0
def handle_message(mlist, id, action, comment=None, forward=None):
    message_store = getUtility(IMessageStore)
    requestdb = IListRequests(mlist)
    key, msgdata = requestdb.get_request(id)
    # Handle the action.
    rejection = None
    message_id = msgdata['_mod_message_id']
    sender = msgdata['_mod_sender']
    subject = msgdata['_mod_subject']
    keep = False
    if action in (Action.defer, Action.hold):
        # Nothing to do, but preserve the message for later.
        keep = True
    elif action is Action.discard:
        rejection = 'Discarded'
    elif action is Action.reject:
        rejection = 'Refused'
        member = mlist.members.get_member(sender)
        if member:
            language = member.preferred_language
        else:
            language = None
        send_rejection(
            mlist, _('Posting of your message titled "$subject"'),
            sender, comment or _('[No reason given]'), language)
    elif action is Action.accept:
        # Start by getting the message from the message store.
        msg = message_store.get_message_by_id(message_id)
        # Delete moderation-specific entries from the message metadata.
        for key in list(msgdata):
            if key.startswith('_mod_'):
                del msgdata[key]
        # Add some metadata to indicate this message has now been approved.
        msgdata['approved'] = True
        msgdata['moderator_approved'] = True
        # Calculate a new filebase for the approved message, otherwise
        # delivery errors will cause duplicates.
        if 'filebase' in msgdata:
            del msgdata['filebase']
        # Queue the file for delivery.  Trying to deliver the message directly
        # here can lead to a huge delay in web turnaround.  Log the moderation
        # and add a header.
        msg['X-Mailman-Approved-At'] = formatdate(
            time.mktime(now().timetuple()), localtime=True)
        vlog.info('held message approved, message-id: %s',
                  msg.get('message-id', 'n/a'))
        # Stick the message back in the incoming queue for further
        # processing.
        config.switchboards['pipeline'].enqueue(msg, _metadata=msgdata)
    else:
        raise AssertionError('Unexpected action: {0}'.format(action))
    # Forward the message.
    if forward:
        # Get a copy of the original message from the message store.
        msg = message_store.get_message_by_id(message_id)
        # It's possible the forwarding address list is a comma separated list
        # of display_name/address pairs.
        addresses = [addr[1] for addr in getaddresses(forward)]
        language = mlist.preferred_language
        if len(addresses) == 1:
            # If the address getting the forwarded message is a member of
            # the list, we want the headers of the outer message to be
            # encoded in their language.  Otherwise it'll be the preferred
            # language of the mailing list.  This is better than sending a
            # separate message per recipient.
            member = mlist.members.get_member(addresses[0])
            if member:
                language = member.preferred_language
        with _.using(language.code):
            fmsg = UserNotification(
                addresses, mlist.bounces_address,
                _('Forward of moderated message'),
                lang=language)
        fmsg.set_type('message/rfc822')
        fmsg.attach(msg)
        fmsg.send(mlist)
    # Delete the request if it's not being kept.
    if not keep:
        requestdb.delete_request(id)
    # Log the rejection
    if rejection:
        note = """%s: %s posting:
\tFrom: %s
\tSubject: %s"""
        if comment:
            note += '\n\tReason: ' + comment
        vlog.info(note, mlist.fqdn_listname, rejection, sender, subject)
Beispiel #48
0
 def _dispose(self, mlist, msg, msgdata):
     message_id = msg.get('message-id', 'n/a')
     # The policy here is similar to the Replybot policy.  If a message has
     # "Precedence: bulk|junk|list" and no "X-Ack: yes" header, we discard
     # the command message.
     precedence = msg.get('precedence', '').lower()
     ack = msg.get('x-ack', '').lower()
     if ack != 'yes' and precedence in ('bulk', 'junk', 'list'):
         log.info('%s Precedence: %s message discarded by: %s',
                  message_id, precedence, mlist.request_address)
         return False
     # Do replybot for commands.
     replybot = config.handlers['replybot']
     replybot.process(mlist, msg, msgdata)
     if mlist.autorespond_requests == ResponseAction.respond_and_discard:
         # Respond and discard.
         log.info('%s -request message replied and discarded', message_id)
         return False
     # Now craft the response and process the command lines.
     charset = msg.get_param('charset')
     if charset is None:
         charset = 'us-ascii'
     results = Results(charset)
     # Include just a few key pieces of information from the original: the
     # sender, date, and message id.
     print(_('- Original message details:'), file=results)
     subject = msg.get('subject', 'n/a')                      # noqa: F841
     date = msg.get('date', 'n/a')                            # noqa: F841
     from_ = msg.get('from', 'n/a')                           # noqa: F841
     print(_('    From: $from_'), file=results)
     print(_('    Subject: $subject'), file=results)
     print(_('    Date: $date'), file=results)
     print(_('    Message-ID: $message_id'), file=results)
     print(_('\n- Results:'), file=results)
     finder = CommandFinder(msg, msgdata, results)
     for parts in finder:
         command = None
         # Try to find a command on this line.  There may be a Re: prefix
         # (possibly internationalized) so try with the first and second
         # words on the line.
         if len(parts) > 0:
             command_name = parts.pop(0)
             command = config.commands.get(command_name)
         if command is None and len(parts) > 0:
             command_name = parts.pop(0)
             command = config.commands.get(command_name)
         if command is None:
             print(_('No such command: $command_name'), file=results)
         else:
             status = command.process(
                 mlist, msg, msgdata, parts, results)
             assert status in ContinueProcessing, (
                 'Invalid status: %s' % status)
             if status == ContinueProcessing.no:
                 break
     # All done.  Strip blank lines and send the response.
     lines = [line.strip() for line in finder.command_lines if line]
     if len(lines) > 0:
         print(_('\n- Unprocessed:'), file=results)
         for line in lines:
             print(line, file=results)
     lines = [line.strip() for line in finder.ignored_lines if line]
     if len(lines) > 0:
         print(_('\n- Ignored:'), file=results)
         for line in lines:
             print(line, file=results)
     print(_('\n- Done.'), file=results)
     # Send a reply, but do not attach the original message.  This is a
     # compromise because the original message is often helpful in tracking
     # down problems, but it's also a vector for backscatter spam.
     language = getUtility(ILanguageManager)[msgdata['lang']]
     reply = UserNotification(msg.sender, mlist.bounces_address,
                              _('The results of your email commands'),
                              lang=language)
     cte = msg.get('content-transfer-encoding')
     if cte is not None:
         reply['Content-Transfer-Encoding'] = cte
     # Find a charset for the response body.  Try the original message's
     # charset first, then ascii, then latin-1 and finally falling back to
     # utf-8.
     reply_body = str(results)
     for charset in (results.charset, 'us-ascii', 'latin-1'):
         with suppress(UnicodeError):
             reply_body.encode(charset)
             break
     else:
         charset = 'utf-8'
     reply.set_payload(reply_body, charset=charset)
     reply.send(mlist)
Beispiel #49
0
 def _dispose(self, mlist, msg, msgdata):
     message_id = msg.get("message-id", "n/a")
     # The policy here is similar to the Replybot policy.  If a message has
     # "Precedence: bulk|junk|list" and no "X-Ack: yes" header, we discard
     # the command message.
     precedence = msg.get("precedence", "").lower()
     ack = msg.get("x-ack", "").lower()
     if ack != "yes" and precedence in ("bulk", "junk", "list"):
         log.info("%s Precedence: %s message discarded by: %s", message_id, precedence, mlist.request_address)
         return False
     # Do replybot for commands.
     replybot = config.handlers["replybot"]
     replybot.process(mlist, msg, msgdata)
     if mlist.autorespond_requests == 1:
         # Respond and discard.
         log.info("%s -request message replied and discard", message_id)
         return False
     # Now craft the response and process the command lines.
     charset = msg.get_param("charset")
     if charset is None:
         charset = "us-ascii"
     results = Results(charset)
     # Include just a few key pieces of information from the original: the
     # sender, date, and message id.
     print(_("- Original message details:"), file=results)
     subject = msg.get("subject", "n/a")
     date = msg.get("date", "n/a")
     from_ = msg.get("from", "n/a")
     print(_("    From: $from_"), file=results)
     print(_("    Subject: $subject"), file=results)
     print(_("    Date: $date"), file=results)
     print(_("    Message-ID: $message_id"), file=results)
     print(_("\n- Results:"), file=results)
     finder = CommandFinder(msg, msgdata, results)
     for parts in finder:
         command = None
         # Try to find a command on this line.  There may be a Re: prefix
         # (possibly internationalized) so try with the first and second
         # words on the line.
         if len(parts) > 0:
             command_name = parts.pop(0)
             command = config.commands.get(command_name)
         if command is None and len(parts) > 0:
             command_name = parts.pop(0)
             command = config.commands.get(command_name)
         if command is None:
             print(_("No such command: $command_name"), file=results)
         else:
             status = command.process(mlist, msg, msgdata, parts, results)
             assert status in ContinueProcessing, "Invalid status: %s" % status
             if status == ContinueProcessing.no:
                 break
     # All done.  Strip blank lines and send the response.
     lines = [line.strip() for line in finder.command_lines if line]
     if len(lines) > 0:
         print(_("\n- Unprocessed:"), file=results)
         for line in lines:
             print(line, file=results)
     lines = [line.strip() for line in finder.ignored_lines if line]
     if len(lines) > 0:
         print(_("\n- Ignored:"), file=results)
         for line in lines:
             print(line, file=results)
     print(_("\n- Done."), file=results)
     # Send a reply, but do not attach the original message.  This is a
     # compromise because the original message is often helpful in tracking
     # down problems, but it's also a vector for backscatter spam.
     language = getUtility(ILanguageManager)[msgdata["lang"]]
     reply = UserNotification(
         msg.sender, mlist.bounces_address, _("The results of your email commands"), lang=language
     )
     cte = msg.get("content-transfer-encoding")
     if cte is not None:
         reply["Content-Transfer-Encoding"] = cte
     # Find a charset for the response body.  Try the original message's
     # charset first, then ascii, then latin-1 and finally falling back to
     # utf-8.
     reply_body = str(results)
     for charset in (results.charset, "us-ascii", "latin-1"):
         try:
             reply_body.encode(charset)
             break
         except UnicodeError:
             pass
     else:
         charset = "utf-8"
     reply.set_payload(reply_body, charset=charset)
     reply.send(mlist)
Beispiel #50
0
    def _process(self, mlist, msg, msgdata):
        """See `TerminalChainBase`."""
        # Start by decorating the message with a header that contains a list
        # of all the rules that matched.  These metadata could be None or an
        # empty list.
        rule_hits = msgdata.get('rule_hits')
        if rule_hits:
            msg['X-Mailman-Rule-Hits'] = SEMISPACE.join(rule_hits)
        rule_misses = msgdata.get('rule_misses')
        if rule_misses:
            msg['X-Mailman-Rule-Misses'] = SEMISPACE.join(rule_misses)
        # Hold the message by adding it to the list's request database.
        request_id = hold_message(mlist, msg, msgdata, None)
        # Calculate a confirmation token to send to the author of the
        # message.
        pendable = HeldMessagePendable(id=request_id)
        token = getUtility(IPendings).add(pendable)
        # Get the language to send the response in.  If the sender is a
        # member, then send it in the member's language, otherwise send it in
        # the mailing list's preferred language.
        member = mlist.members.get_member(msg.sender)
        language = (member.preferred_language
                    if member else mlist.preferred_language)
        # A substitution dictionary for the email templates.
        charset = mlist.preferred_language.charset
        original_subject = msg.get('subject')
        if original_subject is None:
            original_subject = _('(no subject)')
        else:
            # This must be encoded to the mailing list's perferred charset,
            # ignoring incompatible characters, otherwise when creating the
            # notification messages, we could get a Unicode error.
            oneline_subject = oneline(original_subject, in_unicode=True)
            bytes_subject = oneline_subject.encode(charset, 'replace')
            original_subject = bytes_subject.decode(charset)
        substitutions = dict(
            subject=original_subject,
            sender_email=msg.sender,
            reasons=_compose_reasons(msgdata),
            # For backward compatibility.
            sender=msg.sender,
            )
        # At this point the message is held, but now we have to craft at least
        # two responses.  The first will go to the original author of the
        # message and it will contain the token allowing them to approve or
        # discard the message.  The second one will go to the moderators of
        # the mailing list, if the list is so configured.
        #
        # Start by possibly sending a response to the message author.  There
        # are several reasons why we might not go through with this.  If the
        # message was gated from NNTP, the author may not even know about this
        # list, so don't spam them.  If the author specifically requested that
        # acknowledgments not be sent, or if the message was bulk email, then
        # we do not send the response.  It's also possible that either the
        # mailing list, or the author (if they are a member) have been
        # configured to not send such responses.
        if (not msgdata.get('fromusenet') and
                can_acknowledge(msg) and
                mlist.respond_to_post_requests and
                autorespond_to_sender(mlist, msg.sender, language)):
            # We can respond to the sender with a message indicating their
            # posting was held.
            subject = _(
              'Your message to $mlist.fqdn_listname awaits moderator approval')
            send_language_code = msgdata.get('lang', language.code)
            template = getUtility(ITemplateLoader).get(
                'list:user:notice:hold', mlist,
                language=send_language_code)
            text = wrap(expand(template, mlist, dict(
                language=send_language_code,
                **substitutions)))
            adminaddr = mlist.bounces_address
            nmsg = UserNotification(
                msg.sender, adminaddr, subject, text,
                getUtility(ILanguageManager)[send_language_code])
            nmsg.send(mlist)
        # Now the message for the list moderators.  This one should appear to
        # come from <list>-owner since we really don't need to do bounce
        # processing on it.
        if mlist.admin_immed_notify:
            # Now let's temporarily set the language context to that which the
            # administrators are expecting.
            with _.using(mlist.preferred_language.code):
                language = mlist.preferred_language
                charset = language.charset
                substitutions['subject'] = original_subject
                # We need to regenerate or re-translate a few values in the
                # substitution dictionary.
                substitutions['reasons'] = _compose_reasons(msgdata, 55)
                # craft the admin notification message and deliver it
                subject = _(
                    '$mlist.fqdn_listname post from $msg.sender requires '
                    'approval')
                nmsg = UserNotification(mlist.owner_address,
                                        mlist.owner_address,
                                        subject, lang=language)
                nmsg.set_type('multipart/mixed')
                template = getUtility(ITemplateLoader).get(
                    'list:admin:action:post', mlist)
                text = MIMEText(expand(template, mlist, substitutions),
                                _charset=charset)
                dmsg = MIMEText(wrap(_("""\
If you reply to this message, keeping the Subject: header intact, Mailman will
discard the held message.  Do this if the message is spam.  If you reply to
this message and include an Approved: header with the list password in it, the
message will be approved for posting to the list.  The Approved: header can
also appear in the first line of the body of the reply.""")),
                                _charset=language.charset)
                dmsg['Subject'] = 'confirm ' + token
                dmsg['From'] = mlist.request_address
                dmsg['Date'] = formatdate(localtime=True)
                dmsg['Message-ID'] = make_msgid()
                nmsg.attach(text)
                nmsg.attach(MIMEMessage(msg))
                nmsg.attach(MIMEMessage(dmsg))
                nmsg.send(mlist, **dict(tomoderators=True))
        # Log the held message.  Log messages are not translated, so recast
        # the reasons in the English.
        with _.using('en'):
            reasons = msgdata.get('moderation_reasons', ['N/A'])
            log.info('HOLD: %s post from %s held, message-id=%s: %s',
                     mlist.fqdn_listname, msg.sender,
                     msg.get('message-id', 'n/a'), SEMISPACE.join(reasons))
        notify(HoldEvent(mlist, msg, msgdata, self))
Beispiel #51
0
 def _dispose(self, mlist, msg, msgdata):
     message_id = msg.get('message-id', 'n/a')
     # The policy here is similar to the Replybot policy.  If a message has
     # "Precedence: bulk|junk|list" and no "X-Ack: yes" header, we discard
     # the command message.
     precedence = msg.get('precedence', '').lower()
     ack = msg.get('x-ack', '').lower()
     if ack <> 'yes' and precedence in ('bulk', 'junk', 'list'):
         log.info('%s Precedence: %s message discarded by: %s',
                  message_id, precedence, mlist.request_address)
         return False
     # Do replybot for commands.
     replybot = config.handlers['replybot']
     replybot.process(mlist, msg, msgdata)
     if mlist.autorespond_requests == 1:
         # Respond and discard.
         log.info('%s -request message replied and discard', message_id)
         return False
     # Now craft the response and process the command lines.
     charset = msg.get_param('charset')
     if charset is None:
         charset = 'us-ascii'
     results = Results(charset)
     # Include just a few key pieces of information from the original: the
     # sender, date, and message id.
     print(_('- Original message details:'), file=results)
     subject = msg.get('subject', 'n/a')
     date = msg.get('date', 'n/a')
     from_ = msg.get('from', 'n/a')
     print(_('    From: $from_'), file=results)
     print(_('    Subject: $subject'), file=results)
     print(_('    Date: $date'), file=results)
     print(_('    Message-ID: $message_id'), file=results)
     print(_('\n- Results:'), file=results)
     finder = CommandFinder(msg, msgdata, results)
     for parts in finder:
         command = None
         # Try to find a command on this line.  There may be a Re: prefix
         # (possibly internationalized) so try with the first and second
         # words on the line.
         if len(parts) > 0:
             command_name = parts.pop(0)
             command = config.commands.get(command_name)
         if command is None and len(parts) > 0:
             command_name = parts.pop(0)
             command = config.commands.get(command_name)
         if command is None:
             print(_('No such command: $command_name'), file=results)
         else:
             status = command.process(
                 mlist, msg, msgdata, parts, results)
             assert status in ContinueProcessing, (
                 'Invalid status: %s' % status)
             if status == ContinueProcessing.no:
                 break
     # All done.  Strip blank lines and send the response.
     lines = filter(None, (line.strip() for line in finder.command_lines))
     if len(lines) > 0:
         print(_('\n- Unprocessed:'), file=results)
         for line in lines:
             print(line, file=results)
     lines = filter(None, (line.strip() for line in finder.ignored_lines))
     if len(lines) > 0:
         print(_('\n- Ignored:'), file=results)
         for line in lines:
             print(line, file=results)
     print(_('\n- Done.'), file=results)
     # Send a reply, but do not attach the original message.  This is a
     # compromise because the original message is often helpful in tracking
     # down problems, but it's also a vector for backscatter spam.
     language = getUtility(ILanguageManager)[msgdata['lang']]
     reply = UserNotification(msg.sender, mlist.bounces_address,
                              _('The results of your email commands'),
                              lang=language)
     cte = msg.get('content-transfer-encoding')
     if cte is not None:
         reply['Content-Transfer-Encoding'] = cte
     # Find a charset for the response body.  Try the original message's
     # charset first, then ascii, then latin-1 and finally falling back to
     # utf-8.
     reply_body = unicode(results)
     for charset in (results.charset, 'us-ascii', 'latin-1'):
         try:
             reply_body.encode(charset)
             break
         except UnicodeError:
             pass
     else:
         charset = 'utf-8'
     reply.set_payload(reply_body, charset=charset)
     reply.send(mlist)
Beispiel #52
0
 def process(self, args):
     """See `ICLISubCommand`."""
     language_code = args.language if args.language is not None else system_preferences.preferred_language.code
     # Make sure that the selected language code is known.
     if language_code not in getUtility(ILanguageManager).codes:
         self.parser.error(_("Invalid language code: $language_code"))
         return
     assert len(args.listname) == 1, "Unexpected positional arguments: %s" % args.listname
     # Check to see if the domain exists or not.
     fqdn_listname = args.listname[0]
     listname, at, domain = fqdn_listname.partition("@")
     domain_manager = getUtility(IDomainManager)
     if domain_manager.get(domain) is None and args.domain:
         domain_manager.add(domain)
     # Validate the owner email addresses.  The problem with doing this
     # check in create_list() is that you wouldn't be able to distinguish
     # between an InvalidEmailAddressError for the list name or the
     # owners.  I suppose we could subclass that exception though.
     if args.owners:
         validator = getUtility(IEmailValidator)
         invalid_owners = [owner for owner in args.owners if not validator.is_valid(owner)]
         if invalid_owners:
             invalid = COMMASPACE.join(sorted(invalid_owners))  # noqa
             self.parser.error(_("Illegal owner addresses: $invalid"))
             return
     try:
         mlist = create_list(fqdn_listname, args.owners)
     except InvalidEmailAddressError:
         self.parser.error(_("Illegal list name: $fqdn_listname"))
         return
     except ListAlreadyExistsError:
         self.parser.error(_("List already exists: $fqdn_listname"))
         return
     except BadDomainSpecificationError as domain:
         self.parser.error(_("Undefined domain: $domain"))
         return
     # Find the language associated with the code, then set the mailing
     # list's preferred language to that.
     language_manager = getUtility(ILanguageManager)
     with transaction():
         mlist.preferred_language = language_manager[language_code]
     # Do the notification.
     if not args.quiet:
         print(_("Created mailing list: $mlist.fqdn_listname"))
     if args.notify:
         d = dict(
             listname=mlist.fqdn_listname,  # noqa
             admin_url=mlist.script_url("admin"),  # noqa
             listinfo_url=mlist.script_url("listinfo"),  # noqa
             requestaddr=mlist.request_address,  # noqa
             siteowner=mlist.no_reply_address,  # noqa
         )
         text = make("newlist.txt", mailing_list=mlist, **d)
         # Set the I18N language to the list's preferred language so the
         # header will match the template language.  Stashing and restoring
         # the old translation context is just (healthy? :) paranoia.
         with _.using(mlist.preferred_language.code):
             msg = UserNotification(
                 args.owners,
                 mlist.no_reply_address,
                 _("Your new mailing list: $fqdn_listname"),
                 text,
                 mlist.preferred_language,
             )
             msg.send(mlist)
def send_probe(member, msg=None, message_id=None):
    """Send a VERP probe to the member.

    :param member: The member to send the probe to.  From this object, both
        the user and the mailing list can be determined.
    :type member: IMember
    :param msg: The bouncing message that caused the probe to be sent.
    :type msg:
    :param message_id: MessageID of the bouncing message.
    :type message_id: str
    :return: The token representing this probe in the pendings database.
    :rtype: string
    """
    if (message_id or msg) is None:
        raise ValueError('Required at least one of "message_id" and "msg".')
    mlist = getUtility(IListManager).get_by_list_id(
        member.mailing_list.list_id)
    template = getUtility(ITemplateLoader).get(
        'list:user:notice:probe', mlist,
        language=member.preferred_language.code,
        # For backward compatibility.
        code=member.preferred_language.code,
        )
    text = wrap(expand(template, mlist, dict(
        sender_email=member.address.email,
        # For backward compatibility.
        address=member.address.email,
        email=member.address.email,
        owneraddr=mlist.owner_address,
        )))
    if message_id is None:
        message_id = msg['message-id']
    if isinstance(message_id, bytes):
        message_id = message_id.decode('ascii')
    pendable = _ProbePendable(
        # We can only pend unicodes.
        member_id=member.member_id.hex,
        message_id=message_id,
        )
    token = getUtility(IPendings).add(pendable)
    mailbox, domain_parts = split_email(mlist.bounces_address)
    probe_sender = Template(config.mta.verp_probe_format).safe_substitute(
        bounces=mailbox,
        token=token,
        domain=DOT.join(domain_parts),
        )
    # Calculate the Subject header, in the member's preferred language.
    with _.using(member.preferred_language.code):
        subject = _('$mlist.display_name mailing list probe message')
    # Craft the probe message.  This will be a multipart where the first part
    # is the probe text and the second part is the message that caused this
    # probe to be sent, if it provied.
    probe = UserNotification(member.address.email, probe_sender,
                             subject, lang=member.preferred_language)
    probe.set_type('multipart/mixed')
    notice = MIMEText(text, _charset=member.preferred_language.charset)
    probe.attach(notice)
    if msg is not None:
        probe.attach(MIMEMessage(msg))
    # Probes should not have the Precedence: bulk header.
    probe.send(mlist, sender=probe_sender, verp=False, probe_token=token,
               add_precedence=False)
    # When we send a probe, we reset the score.
    member.bounce_score = 0
    return token