Example #1
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
Example #2
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)
Example #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)
Example #4
0
def ascii_header(mlist, msgdata, subject, prefix, prefix_pattern, ws):
    if mlist.preferred_language.charset not in ASCII_CHARSETS:
        return None
    for chunk, charset in decode_header(subject.encode()):
        if charset not in ASCII_CHARSETS:
            return None
    subject_text = EMPTYSTRING.join(str(subject).splitlines())
    rematch = re.match(RE_PATTERN, subject_text, re.I)
    if rematch:
        subject_text = subject_text[rematch.end():]
        recolon = 'Re: '
    else:
        recolon = ''
    # At this point, the subject may become null if someone posted mail
    # with "Subject: [subject prefix]".
    if subject_text.strip() == '':
        with _.using(mlist.preferred_language.code):
            subject_text = _('(no subject)')
    else:
        subject_text = re.sub(prefix_pattern, '', subject_text)
    msgdata['stripped_subject'] = subject_text
    lines = subject_text.splitlines()
    first_line = [lines[0]]
    if recolon:
        first_line.insert(0, recolon)
    if prefix:
        first_line.insert(0, prefix)
    subject_text = EMPTYSTRING.join(first_line)
    return Header(subject_text, continuation_ws=ws)
def ascii_header(mlist, msgdata, subject, prefix, prefix_pattern, ws):
    if mlist.preferred_language.charset not in ASCII_CHARSETS:
        return None
    for chunk, charset in decode_header(subject.encode()):
        if charset not in ASCII_CHARSETS:
            return None
    subject_text = EMPTYSTRING.join(str(subject).splitlines())
    # At this point, the subject may become null if someone posted mail
    # with "Subject: [subject prefix]".
    if subject_text.strip() == '':
        with _.using(mlist.preferred_language.code):
            subject_text = _('(no subject)')
    else:
        subject_text = re.sub(prefix_pattern, '', subject_text)
    msgdata['stripped_subject'] = subject_text
    rematch = re.match(RE_PATTERN, subject_text, re.I)
    if rematch:
        subject_text = subject_text[rematch.end():]
        recolon = 'Re: '
    else:
        recolon = ''
    lines = subject_text.splitlines()
    # If the subject was only the prefix or Re:, the text could be null.
    first_line = []
    if lines:
        first_line = [lines[0]]
    if recolon:
        first_line.insert(0, recolon)
    if prefix:
        first_line.insert(0, prefix)
    subject_text = EMPTYSTRING.join(first_line)
    return Header(subject_text, continuation_ws=ws)
Example #6
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)
Example #7
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)
Example #8
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
Example #9
0
 def _process_one_file(self, msg, msgdata):
     """See `IRunner`."""
     # Do some common sanity checking on the message metadata.  It's got to
     # be destined for a particular mailing list.  This switchboard is used
     # to shunt off badly formatted messages.  We don't want to just trash
     # them because they may be fixable with human intervention.  Just get
     # them out of our sight.
     #
     # Find out which mailing list this message is destined for.
     mlist = None
     missing = object()
     # First try to dig out the target list by id.  If there's no list-id
     # in the metadata, fall back to the fqdn list name for backward
     # compatibility.
     list_manager = getUtility(IListManager)
     list_id = msgdata.get('listid', missing)
     fqdn_listname = None
     if list_id is missing:
         fqdn_listname = msgdata.get('listname', missing)
         # XXX Deprecate.
         if fqdn_listname is not missing:
             mlist = list_manager.get(fqdn_listname)
     else:
         mlist = list_manager.get_by_list_id(list_id)
     if mlist is None:
         identifier = (list_id if list_id is not None else fqdn_listname)
         elog.error(
             '%s runner "%s" shunting message for missing list: %s',
             msg['message-id'], self.name, identifier)
         config.switchboards['shunt'].enqueue(msg, msgdata)
         return
     # Now process this message.  We also want to set up the language
     # context for this message.  The context will be the preferred
     # language for the user if the sender is a member of the list, or it
     # will be the list's preferred language.  However, we must take
     # special care to reset the defaults, otherwise subsequent messages
     # may be translated incorrectly.
     if mlist is None:
         language_manager = getUtility(ILanguageManager)
         language = language_manager[config.mailman.default_language]
     elif msg.sender:
         member = mlist.members.get_member(msg.sender)
         language = (member.preferred_language
                     if member is not None
                     else mlist.preferred_language)
     else:
         language = mlist.preferred_language
     with _.using(language.code):
         msgdata['lang'] = language.code
         try:
             keepqueued = self._dispose(mlist, msg, msgdata)
         except Exception as error:
             # Trigger the Zope event and re-raise
             notify(RunnerCrashEvent(self, mlist, msg, msgdata, error))
             raise
     if keepqueued:
         self.switchboard.enqueue(msg, msgdata)
Example #10
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
Example #11
0
 def _process_one_file(self, msg, msgdata):
     """See `IRunner`."""
     # Do some common sanity checking on the message metadata.  It's got to
     # be destined for a particular mailing list.  This switchboard is used
     # to shunt off badly formatted messages.  We don't want to just trash
     # them because they may be fixable with human intervention.  Just get
     # them out of our sight.
     #
     # Find out which mailing list this message is destined for.
     mlist = None
     missing = object()
     # First try to dig out the target list by id.  If there's no list-id
     # in the metadata, fall back to the fqdn list name for backward
     # compatibility.
     list_manager = getUtility(IListManager)
     list_id = msgdata.get('listid', missing)
     fqdn_listname = None
     if list_id is missing:
         fqdn_listname = msgdata.get('listname', missing)
         # XXX Deprecate.
         if fqdn_listname is not missing:
             mlist = list_manager.get(fqdn_listname)
     else:
         mlist = list_manager.get_by_list_id(list_id)
     if mlist is None:
         identifier = (list_id if list_id is not None else fqdn_listname)
         elog.error(
             '%s runner "%s" shunting message for missing list: %s',
             msg['message-id'], self.name, identifier)
         config.switchboards['shunt'].enqueue(msg, msgdata)
         return
     # Now process this message.  We also want to set up the language
     # context for this message.  The context will be the preferred
     # language for the user if the sender is a member of the list, or it
     # will be the list's preferred language.  However, we must take
     # special care to reset the defaults, otherwise subsequent messages
     # may be translated incorrectly.
     if mlist is None:
         language_manager = getUtility(ILanguageManager)
         language = language_manager[config.mailman.default_language]
     elif msg.sender:
         member = mlist.members.get_member(msg.sender)
         language = (member.preferred_language
                     if member is not None
                     else mlist.preferred_language)
     else:
         language = mlist.preferred_language
     with _.using(language.code):
         msgdata['lang'] = language.code
         try:
             keepqueued = self._dispose(mlist, msg, msgdata)
         except Exception as error:
             # Trigger the Zope event and re-raise
             notify(RunnerCrashEvent(self, mlist, msg, msgdata, error))
             raise
     if keepqueued:
         self.switchboard.enqueue(msg, msgdata)
Example #12
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
def send_admin_subscription_notice(mlist, address, display_name):
    """Send the list administrators a subscription notice.

    :param mlist: The mailing list.
    :type mlist: IMailingList
    :param address: The address being subscribed.
    :type address: string
    :param display_name: The name of the subscriber.
    :type display_name: string
    """
    with _.using(mlist.preferred_language.code):
        subject = _('$mlist.display_name subscription notification')
    text = expand(
        getUtility(ITemplateLoader).get('list:admin:notice:subscribe', mlist),
        mlist, dict(member=formataddr((display_name, address)), ))
    msg = OwnerNotification(mlist, subject, text, roster=mlist.administrators)
    msg.send(mlist)
def send_admin_disable_notice(mlist, address, display_name):
    """Send the list administrators a membership disabled by-bounce notice.

    :param mlist: The mailing list
    :type mlist: IMailingList
    :param address: The address of the member
    :type address: string
    :param display_name: The name of the subscriber
    :type display_name: string
    """
    member = formataddr((display_name, address))
    data = {'member': member}
    with _.using(mlist.preferred_language.code):
        subject = _('$member\'s subscription disabled on $mlist.display_name')
    text = expand(
        getUtility(ITemplateLoader).get('list:admin:notice:disable', mlist),
        mlist, data)
    msg = OwnerNotification(mlist, subject, text, roster=mlist.administrators)
    msg.send(mlist)
Example #15
0
def send_admin_subscription_notice(mlist, address, display_name):
    """Send the list administrators a subscription notice.

    :param mlist: The mailing list.
    :type mlist: IMailingList
    :param address: The address being subscribed.
    :type address: string
    :param display_name: The name of the subscriber.
    :type display_name: string
    """
    with _.using(mlist.preferred_language.code):
        subject = _('$mlist.display_name subscription notification')
    text = make('adminsubscribeack.txt',
                mailing_list=mlist,
                listname=mlist.display_name,
                member=formataddr((display_name, address)),
                )
    msg = OwnerNotification(mlist, subject, text, roster=mlist.administrators)
    msg.send(mlist)
def send_admin_removal_notice(mlist, address, display_name):
    """Send the list administrators a membership removed due to bounce notice.

    :param mlist: The mailing list.
    :type mlist: IMailingList
    :param address: The address of the member
    :type address: string
    :param display_name: The name of the subscriber
    :type display_name: string
    """
    member = formataddr((display_name, address))
    data = {'member': member, 'mlist': mlist.display_name}
    with _.using(mlist.preferred_language.code):
        subject = _('$member unsubscribed from ${mlist.display_name} '
                    'mailing list due to bounces')
    text = expand(
        getUtility(ITemplateLoader).get('list:admin:notice:removal', mlist),
        mlist, data)
    msg = OwnerNotification(mlist, subject, text, roster=mlist.administrators)
    msg.send(mlist)
Example #17
0
def send_admin_subscription_notice(mlist, address, display_name):
    """Send the list administrators a subscription notice.

    :param mlist: The mailing list.
    :type mlist: IMailingList
    :param address: The address being subscribed.
    :type address: string
    :param display_name: The name of the subscriber.
    :type display_name: string
    """
    with _.using(mlist.preferred_language.code):
        subject = _('$mlist.display_name subscription notification')
    text = make(
        'adminsubscribeack.txt',
        mailing_list=mlist,
        listname=mlist.display_name,
        member=formataddr((display_name, address)),
    )
    msg = OwnerNotification(mlist, subject, text, roster=mlist.administrators)
    msg.send(mlist)
Example #18
0
 def _process_one_file(self, msg, msgdata):
     """See `IRunner`."""
     # Do some common sanity checking on the message metadata.  It's got to
     # be destined for a particular mailing list.  This switchboard is used
     # to shunt off badly formatted messages.  We don't want to just trash
     # them because they may be fixable with human intervention.  Just get
     # them out of our sight.
     #
     # Find out which mailing list this message is destined for.
     missing = object()
     listname = msgdata.get("listname", missing)
     mlist = None if listname is missing else getUtility(IListManager).get(unicode(listname))
     if mlist is None:
         elog.error(
             '%s runner "%s" shunting message for missing list: %s',
             msg["message-id"],
             self.name,
             ("n/a" if listname is missing else listname),
         )
         config.switchboards["shunt"].enqueue(msg, msgdata)
         return
     # Now process this message.  We also want to set up the language
     # context for this message.  The context will be the preferred
     # language for the user if the sender is a member of the list, or it
     # will be the list's preferred language.  However, we must take
     # special care to reset the defaults, otherwise subsequent messages
     # may be translated incorrectly.
     if mlist is None:
         language_manager = getUtility(ILanguageManager)
         language = language_manager[config.mailman.default_language]
     elif msg.sender:
         member = mlist.members.get_member(msg.sender)
         language = member.preferred_language if member is not None else mlist.preferred_language
     else:
         language = mlist.preferred_language
     with _.using(language.code):
         msgdata["lang"] = language.code
         keepqueued = self._dispose(mlist, msg, msgdata)
     if keepqueued:
         self.switchboard.enqueue(msg, msgdata)
Example #19
0
 def _dispose(self, mlist, msg, msgdata):
     """See `IRunner`."""
     volume = msgdata['volume']
     digest_number = msgdata['digest_number']
     # Backslashes make me cry.
     code = mlist.preferred_language.code
     with Mailbox(msgdata['digest_path']) as mailbox, _.using(code):
         # Create the digesters.
         mime_digest = MIMEDigester(mlist, volume, digest_number)
         rfc1153_digest = RFC1153Digester(mlist, volume, digest_number)
         # Cruise through all the messages in the mailbox, first building
         # the table of contents and accumulating Subject: headers and
         # authors.  The question really is whether it's better from a1
         # performance and memory footprint to go through the mailbox once
         # and cache the messages in a list, or to cruise through the
         # mailbox twice.  We'll do the latter, but it's a complete guess.
         count = None
         for count, (key, message) in enumerate(mailbox.iteritems(), 1):
             mime_digest.add_to_toc(message, count)
             rfc1153_digest.add_to_toc(message, count)
         assert count is not None, 'No digest messages?'
         # Add the table of contents.
         mime_digest.add_toc(count)
         rfc1153_digest.add_toc(count)
         # Cruise through the set of messages a second time, adding them to
         # the actual digest.
         for count, (key, message) in enumerate(mailbox.iteritems(), 1):
             mime_digest.add_message(message, count)
             rfc1153_digest.add_message(message, count)
         # Finish up the digests.
         mime = mime_digest.finish()
         rfc1153 = rfc1153_digest.finish()
     # Calculate the recipients lists
     mime_recipients = set()
     rfc1153_recipients = set()
     # When someone turns off digest delivery, they will get one last
     # digest to ensure that there will be no gaps in the messages they
     # receive.
     digest_members = set(mlist.digest_members.members)
     for member in digest_members:
         if member.delivery_status is not DeliveryStatus.enabled:
             continue
         # Send the digest to the case-preserved address of the digest
         # members.
         email_address = member.address.original_email
         if member.delivery_mode == DeliveryMode.plaintext_digests:
             rfc1153_recipients.add(email_address)
         # We currently treat summary_digests the same as mime_digests.
         elif member.delivery_mode in (DeliveryMode.mime_digests,
                                       DeliveryMode.summary_digests):
             mime_recipients.add(email_address)
         else:
             raise AssertionError(
                 'Digest member "{}" unexpected delivery mode: {}'.format(
                     email_address, member.delivery_mode))
     # Add also the folks who are receiving one last digest.
     for address, delivery_mode in mlist.last_digest_recipients:
         if delivery_mode == DeliveryMode.plaintext_digests:
             rfc1153_recipients.add(address.original_email)
         # We currently treat summary_digests the same as mime_digests.
         elif delivery_mode in (DeliveryMode.mime_digests,
                                DeliveryMode.summary_digests):
             mime_recipients.add(address.original_email)
         else:
             raise AssertionError(
                 'OLD recipient "{}" unexpected delivery mode: {}'.format(
                     address, delivery_mode))
     # Send the digests to the virgin queue for final delivery.
     queue = config.switchboards['virgin']
     if len(mime_recipients) > 0:
         queue.enqueue(mime,
                       recipients=mime_recipients,
                       listid=mlist.list_id,
                       isdigest=True)
     if len(rfc1153_recipients) > 0:
         queue.enqueue(rfc1153,
                       recipients=rfc1153_recipients,
                       listid=mlist.list_id,
                       isdigest=True)
     # Remove the digest mbox. (GL #259)
     os.remove(msgdata['digest_path'])
Example #20
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)
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
Example #22
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)
Example #23
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)
Example #24
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))
Example #25
0
def munged_headers(mlist, msg, msgdata):
    # This returns a list of tuples (header, content) where header is the
    # name of a header to be added to or replaced in the wrapper or message
    # for DMARC mitigation.  It sets From: to the string
    # 'original From: display name' via 'list name' <list posting address>
    # and adds the original From: to Reply-To: or Cc: per the following.
    # Our goals for this process are not completely compatible, so we do
    # the best we can.  Our goals are:
    # 1) as long as the list is not anonymous, the original From: address
    #    should be obviously exposed, i.e. not just in a header that MUAs
    #    don't display.
    # 2) the original From: address should not be in a comment or display
    #    name in the new From: because it is claimed that multiple domains
    #    in any fields in From: are indicative of spamminess.  This means
    #    it should be in Reply-To: or Cc:.
    # 3) the behavior of an MUA doing a 'reply' or 'reply all' should be
    #    consistent regardless of whether or not the From: is munged.
    # Goal 3) implies sometimes the original From: should be in Reply-To:
    # and sometimes in Cc:, and even so, this goal won't be achieved in
    # all cases with all MUAs.  In cases of conflict, the above ordering of
    # goals is priority order.
    #
    # Be as robust as possible here.
    all_froms = getaddresses(msg.get_all('from', []))
    # Strip the nulls and bad emails.
    froms = [email for email in all_froms if '@' in email[1]]
    if len(froms) == 1:
        realname, email = original_from = froms[0]
    else:
        # No From: or multiple addresses.  Just punt and take
        # the get_sender result.
        realname = ''
        email = msgdata['original_sender']
        original_from = (realname, email)
    # If there was no display name in the email header, see if we have a
    # matching member with a display name.
    if len(realname) == 0:
        member = mlist.members.get_member(email)
        if member:
            realname = member.display_name or email
        else:
            realname = email
    # Remove the domain from realname if it looks like an email address.
    realname = re.sub(r'@([^ .]+\.)+[^ .]+$', '---', realname)
    # Make a display name and RFC 2047 encode it if necessary.  This is
    # difficult and kludgy.  If the realname came from From: it should be
    # ASCII or RFC 2047 encoded.  If it came from the member record, it should
    # be a string.  If it's from the email address, it should be an ASCII
    # string.  In any case, ensure it's an unencoded string.
    realname_bits = []
    for fragment, charset in decode_header(realname):
        if not charset:
            # Character set should be ASCII, but use iso-8859-1 anyway.
            charset = 'iso-8859-1'
        if not isinstance(fragment, str):
            realname_bits.append(str(fragment, charset, errors='replace'))
        else:
            realname_bits.append(fragment)
    # The member's display name is a string.
    realname = EMPTYSTRING.join(realname_bits)
    # Ensure the i18n context is the list's preferred_language.
    with _.using(mlist.preferred_language.code):
        via = _('$realname via $mlist.display_name')
    # Get an RFC 2047 encoded header string.
    display_name = str(Header(via, mlist.preferred_language.charset))
    value = [('From', formataddr((display_name, mlist.posting_address)))]
    # We've made the munged From:.  Now put the original in Reply-To: or Cc:
    if mlist.reply_goes_to_list is ReplyToMunging.no_munging:
        # Add original from to Reply-To:
        add_to = 'Reply-To'
    else:
        # Add original from to Cc:
        add_to = 'Cc'
    original = getaddresses(msg.get_all(add_to, []))
    if original_from[1] not in [x[1] for x in original]:
        original.append(original_from)
    value.append((add_to, COMMASPACE.join(formataddr(x) for x in original)))
    return value
Example #26
0
 def _dispose(self, mlist, msg, msgdata):
     """See `IRunner`."""
     volume = msgdata['volume']
     digest_number = msgdata['digest_number']
     # Backslashes make me cry.
     code = mlist.preferred_language.code
     with Mailbox(msgdata['digest_path']) as mailbox, _.using(code):
         # Create the digesters.
         mime_digest = MIMEDigester(mlist, volume, digest_number)
         rfc1153_digest = RFC1153Digester(mlist, volume, digest_number)
         # Cruise through all the messages in the mailbox, first building
         # the table of contents and accumulating Subject: headers and
         # authors.  The question really is whether it's better from a1
         # performance and memory footprint to go through the mailbox once
         # and cache the messages in a list, or to cruise through the
         # mailbox twice.  We'll do the latter, but it's a complete guess.
         count = None
         for count, (key, message) in enumerate(mailbox.iteritems(), 1):
             mime_digest.add_to_toc(message, count)
             rfc1153_digest.add_to_toc(message, count)
         assert count is not None, 'No digest messages?'
         # Add the table of contents.
         mime_digest.add_toc(count)
         rfc1153_digest.add_toc(count)
         # Cruise through the set of messages a second time, adding them to
         # the actual digest.
         for count, (key, message) in enumerate(mailbox.iteritems(), 1):
             mime_digest.add_message(message, count)
             rfc1153_digest.add_message(message, count)
         # Finish up the digests.
         mime = mime_digest.finish()
         rfc1153 = rfc1153_digest.finish()
     # Calculate the recipients lists
     mime_recipients = set()
     rfc1153_recipients = set()
     # When someone turns off digest delivery, they will get one last
     # digest to ensure that there will be no gaps in the messages they
     # receive.
     digest_members = set(mlist.digest_members.members)
     for member in digest_members:
         if member.delivery_status is not DeliveryStatus.enabled:
             continue
         # Send the digest to the case-preserved address of the digest
         # members.
         email_address = member.address.original_email
         if member.delivery_mode == DeliveryMode.plaintext_digests:
             rfc1153_recipients.add(email_address)
         # We currently treat summary_digests the same as mime_digests.
         elif member.delivery_mode in (DeliveryMode.mime_digests,
                                       DeliveryMode.summary_digests):
             mime_recipients.add(email_address)
         else:
             raise AssertionError(
                 'Digest member "{0}" unexpected delivery mode: {1}'.format(
                     email_address, member.delivery_mode))
     # Add also the folks who are receiving one last digest.
     for address, delivery_mode in mlist.last_digest_recipients:
         if delivery_mode == DeliveryMode.plaintext_digests:
             rfc1153_recipients.add(address.original_email)
         # We currently treat summary_digests the same as mime_digests.
         elif delivery_mode in (DeliveryMode.mime_digests,
                                DeliveryMode.summary_digests):
             mime_recipients.add(address.original_email)
         else:
             raise AssertionError(
                 'OLD recipient "{0}" unexpected delivery mode: {1}'.format(
                     address, delivery_mode))
     # Send the digests to the virgin queue for final delivery.
     queue = config.switchboards['virgin']
     if len(mime_recipients) > 0:
         queue.enqueue(mime,
                       recipients=mime_recipients,
                       listid=mlist.list_id,
                       isdigest=True)
     if len(rfc1153_recipients) > 0:
         queue.enqueue(rfc1153,
                       recipients=rfc1153_recipients,
                       listid=mlist.list_id,
                       isdigest=True)
Example #27
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))
Example #28
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)