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
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)
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 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)
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
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
def test_substitutions_no_wrap(self): self.assertEqual(make('nowrap.txt', self.mlist, wrap=False, kind='very nice', howmany='a few'), """\ This is a very nice template. It has a few substitutions. It will not be wrapped. """)
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 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_substitutions_no_wrap(self): self.assertEqual( make('nowrap.txt', self.mlist, wrap=False, kind='very nice', howmany='a few'), """\ This is a very nice template. It has a few substitutions. It will not be wrapped. """)
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 delete_member(mlist, email, admin_notif=None, userack=None): """Delete a member right now. :param mlist: The mailing list to remove the member from. :type mlist: `IMailingList` :param email: The email address to unsubscribe. :type email: string :param admin_notif: Whether the list administrator should be notified that this member was deleted. :type admin_notif: bool, or None to let the mailing list's `admin_notify_mchange` attribute decide. :raises NotAMemberError: if the address is not a member of the mailing list. """ if userack is None: userack = mlist.send_goodbye_message if admin_notif is None: admin_notif = mlist.admin_notify_mchanges # Delete a member, for which we know the approval has been made. member = mlist.members.get_member(email) if member is None: raise NotAMemberError(mlist, email) language = member.preferred_language member.unsubscribe() # And send an acknowledgement to the user... if userack: send_goodbye_message(mlist, email, language) # ...and to the administrator. if admin_notif: user = getUtility(IUserManager).get_user(email) display_name = user.display_name subject = _('$mlist.display_name unsubscription notification') text = make( 'adminunsubscribeack.txt', mailing_list=mlist, listname=mlist.display_name, member=formataddr((display_name, email)), ) msg = OwnerNotification(mlist, subject, text, roster=mlist.administrators) msg.send(mlist)
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_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 __init__(self, mlist, volume, digest_number): self._mlist = mlist self._charset = mlist.preferred_language.charset # This will be used in the Subject, so use $-strings. self._digest_id = _( '$mlist.display_name Digest, Vol $volume, Issue $digest_number') self._subject = Header(self._digest_id, self._charset, header_name='Subject') self._message = self._make_message() self._digest_part = self._make_digest_part() self._message['From'] = mlist.request_address self._message['Subject'] = self._subject self._message['To'] = mlist.posting_address self._message['Reply-To'] = mlist.posting_address self._message['Date'] = formatdate(localtime=True) self._message['Message-ID'] = make_msgid() # In the rfc1153 digest, the masthead contains the digest boilerplate # plus any digest header. In the MIME digests, the masthead and # digest header are separate MIME subobjects. In either case, it's # the first thing in the digest, and we can calculate it now, so go # ahead and add it now. self._masthead = make('masthead.txt', mailing_list=mlist, display_name=mlist.display_name, got_list_email=mlist.posting_address, got_listinfo_url=mlist.script_url('listinfo'), got_request_email=mlist.request_address, got_owner_email=mlist.owner_address, ) # Set things up for the table of contents. if mlist.digest_header_uri is not None: try: self._header = decorate(mlist, mlist.digest_header_uri) except URLError: log.exception( 'Digest header decorator URI not found ({}): {}'.format( mlist.fqdn_listname, mlist.digest_header_uri)) self._header = '' self._toc = StringIO() print(_("Today's Topics:\n"), file=self._toc)
def delete_member(mlist, email, admin_notif=None, userack=None): """Delete a member right now. :param mlist: The mailing list to remove the member from. :type mlist: `IMailingList` :param email: The email address to unsubscribe. :type email: string :param admin_notif: Whether the list administrator should be notified that this member was deleted. :type admin_notif: bool, or None to let the mailing list's `admin_notify_mchange` attribute decide. :raises NotAMemberError: if the address is not a member of the mailing list. """ if userack is None: userack = mlist.send_goodbye_message if admin_notif is None: admin_notif = mlist.admin_notify_mchanges # Delete a member, for which we know the approval has been made. member = mlist.members.get_member(email) if member is None: raise NotAMemberError(mlist, email) language = member.preferred_language member.unsubscribe() # And send an acknowledgement to the user... if userack: send_goodbye_message(mlist, email, language) # ...and to the administrator. if admin_notif: user = getUtility(IUserManager).get_user(email) display_name = user.display_name subject = _('$mlist.display_name unsubscription notification') text = make('adminunsubscribeack.txt', mailing_list=mlist, listname=mlist.display_name, member=formataddr((display_name, email)), ) msg = OwnerNotification(mlist, subject, text, roster=mlist.administrators) msg.send(mlist)
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 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 text = make('postack.txt', mailing_list=mlist, language=language.code, wrap=False, subject=oneline(original_subject, in_unicode=True), 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 maybe_forward(mlist, msg): """Possibly forward bounce messages with no recognizable addresses. :param mlist: The mailing list. :type mlist: `IMailingList` :param msg: The bounce message to scan. :type msg: `Message` """ message_id = msg['message-id'] if (mlist.forward_unrecognized_bounces_to is UnrecognizedBounceDisposition.discard): blog.error('Discarding unrecognized bounce: {0}'.format(message_id)) return # The notification is either going to go to the list's administrators # (owners and moderators), or to the site administrators. Most of the # notification is exactly the same in either case. adminurl = mlist.script_url('admin') + '/bounce' subject = _('Uncaught bounce notification') text = MIMEText( make('unrecognized.txt', mlist, adminurl=adminurl), _charset=mlist.preferred_language.charset) attachment = MIMEMessage(msg) if (mlist.forward_unrecognized_bounces_to is UnrecognizedBounceDisposition.administrators): keywords = dict(roster=mlist.administrators) elif (mlist.forward_unrecognized_bounces_to is UnrecognizedBounceDisposition.site_owner): keywords = {} else: raise AssertionError('Invalid forwarding disposition: {0}'.format( mlist.forward_unrecognized_bounces_to)) # Create the notification and send it. notice = OwnerNotification(mlist, subject, **keywords) notice.set_type('multipart/mixed') notice.attach(text) notice.attach(attachment) notice.send(mlist)
def maybe_forward(mlist, msg): """Possibly forward bounce messages with no recognizable addresses. :param mlist: The mailing list. :type mlist: `IMailingList` :param msg: The bounce message to scan. :type msg: `Message` """ message_id = msg['message-id'] if (mlist.forward_unrecognized_bounces_to is UnrecognizedBounceDisposition.discard): blog.error('Discarding unrecognized bounce: {0}'.format(message_id)) return # The notification is either going to go to the list's administrators # (owners and moderators), or to the site administrators. Most of the # notification is exactly the same in either case. adminurl = mlist.script_url('admin') + '/bounce' subject = _('Uncaught bounce notification') text = MIMEText(make('unrecognized.txt', mlist, adminurl=adminurl), _charset=mlist.preferred_language.charset) attachment = MIMEMessage(msg) if (mlist.forward_unrecognized_bounces_to is UnrecognizedBounceDisposition.administrators): keywords = dict(roster=mlist.administrators) elif (mlist.forward_unrecognized_bounces_to is UnrecognizedBounceDisposition.site_owner): keywords = {} else: raise AssertionError('Invalid forwarding disposition: {0}'.format( mlist.forward_unrecognized_bounces_to)) # Create the notification and send it. notice = OwnerNotification(mlist, subject, **keywords) notice.set_type('multipart/mixed') notice.attach(text) notice.attach(attachment) notice.send(mlist)
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
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
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 _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))
def test_no_substitutions(self): self.assertEqual( make('nosub.txt', self.mlist), """\ This is a global template. It has no substitutions. It will be wrapped.""")
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 _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. # XXX How to calculate the reason? 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, reason='XXX', #reason, confirmurl='{0}/{1}'.format(mlist.script_url('confirm'), token), admindb_url=mlist.script_url('admindb'), ) # 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 # We need to regenerate or re-translate a few values in the # substitution dictionary. #d['reason'] = _(reason) # XXX reason substitutions['subject'] = original_subject # 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 # XXX reason reason = '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'), reason) notify(HoldEvent(mlist, msg, msgdata, self))
def test_substitutions(self): self.assertEqual(make('subs.txt', self.mlist, kind='very nice', howmany='a few'), """\ This is a very nice template. It has a few substitutions. It will be wrapped.""")
def test_no_substitutions(self): self.assertEqual(make('nosub.txt', self.mlist), """\ This is a global template. It has no substitutions. It will be wrapped.""")
def test_substitutions(self): self.assertEqual( make('subs.txt', self.mlist, kind='very nice', howmany='a few'), """\ This is a very nice template. It has a few substitutions. It will be wrapped.""")