def add_message(self, msg, count): """Add the message to the digest.""" if count > 1: print(self._separator30, file=self._text) print(file=self._text) # Each message section contains a few headers. for header in config.digests.plain_digest_keep_headers.split(): if header in msg: value = oneline(msg[header], in_unicode=True) value = wrap('{}: {}'.format(header, value)) value = '\n\t'.join(value.split('\n')) print(value, file=self._text) print(file=self._text) # Add the payload. If the decoded payload is empty, this may be a # multipart message. In that case, just stringify it. payload = msg.get_payload(decode=True) if not payload: payload = msg.as_string().split('\n\n', 1)[1] if isinstance(payload, bytes): try: # Do the decoding inside the try/except so that if the charset # conversion fails, we'll just drop back to ascii. charset = msg.get_content_charset('us-ascii') payload = payload.decode(charset, 'replace') except (LookupError, TypeError): # Unknown or empty charset. payload = payload.decode('us-ascii', 'replace') print(payload, file=self._text) if not payload.endswith('\n'): print(file=self._text)
def test_dont_honor_ws(self): text = """\ This is a single paragraph. It consists of several sentences none of which are very long. This paragraph is indented but we don't honor whitespace so it will be filled. And here is a second paragraph which also consists of several sentences. None of these are very long either. """ self.assertEqual( wrap(text, honor_leading_ws=False), """\ This is a single paragraph. It consists of several sentences none of which are very long. This paragraph is indented but we don't honor whitespace so it will be filled. And here is a second paragraph which also consists of several sentences. None of these are very long either.""", )
def check(self, mlist, msg, msgdata): """See `IRule`.""" if mlist.dmarc_mitigate_action is DMARCMitigateAction.no_mitigation: # Don't bother to check if we're not going to do anything. return False dn, addr = parseaddr(msg.get('from')) if maybe_mitigate(mlist, addr): # If dmarc_mitigate_action is discard or reject, this rule fires # and jumps to the 'moderation' chain to do the actual discard. # Otherwise, the rule misses but sets a flag for the dmarc handler # to do the appropriate action. msgdata['dmarc'] = True if mlist.dmarc_mitigate_action is DMARCMitigateAction.discard: msgdata['moderation_action'] = 'discard' msgdata['moderation_reasons'] = [_('DMARC moderation')] elif mlist.dmarc_mitigate_action is DMARCMitigateAction.reject: listowner = mlist.owner_address # noqa F841 reason = (mlist.dmarc_moderation_notice or _( 'You are not allowed to post to this mailing ' 'list From: a domain which publishes a DMARC ' 'policy of reject or quarantine, and your message' ' has been automatically rejected. If you think ' 'that your messages are being rejected in error, ' 'contact the mailing list owner at ${listowner}.')) msgdata['moderation_reasons'] = [wrap(reason)] msgdata['moderation_action'] = 'reject' else: return False msgdata['moderation_sender'] = addr return True return False
def test_honor_ws(self): text = """\ This is a single paragraph. It consists of several sentences none of which are very long. This paragraph is indented so it won't be filled. And here is a second paragraph which also consists of several sentences. None of these are very long either. """ self.assertEqual( wrap(text), """\ This is a single paragraph. It consists of several sentences none of which are very long. This paragraph is indented so it won't be filled. And here is a second paragraph which also consists of several sentences. None of these are very long either.""")
def add_to_toc(self, msg, count): """Add a message to the table of contents.""" subject = msg.get('subject', _('(no subject)')) subject = oneline(subject, in_unicode=True) # Don't include the redundant subject prefix in the toc mo = re.match( '(re:? *)?({0})'.format(re.escape(self._mlist.subject_prefix)), subject, re.IGNORECASE) if mo: subject = subject[:mo.start(2)] + subject[mo.end(2):] # Take only the first author we find. username = '' addresses = getaddresses( [oneline(msg.get('from', ''), in_unicode=True)]) if addresses: username = addresses[0][0] if not username: username = addresses[0][1] if username: username = '******'.format(username) lines = wrap('{:2}. {}'.format(count, subject), 65).split('\n') # See if the user's name can fit on the last line if len(lines[-1]) + len(username) > 70: lines.append(username) else: lines[-1] += username # Add this subject to the accumulating topics first = True for line in lines: if first: print(' ', line, file=self._toc) first = False else: print(' ', line.lstrip(), file=self._toc)
def add_message(self, msg, count): """Add the message to the digest.""" if count > 1: print >> self._text, self._separator30 print >> self._text # Each message section contains a few headers. for header in config.digests.plain_digest_keep_headers.split(): if header in msg: value = oneline(msg[header], in_unicode=True) value = wrap('{0}: {1}'.format(header, value)) value = '\n\t'.join(value.split('\n')) print >> self._text, value print >> self._text # Add the payload. If the decoded payload is empty, this may be a # multipart message. In that case, just stringify it. payload = msg.get_payload(decode=True) payload = (payload if payload else msg.as_string().split('\n\n', 1)[1]) try: charset = msg.get_content_charset('us-ascii') payload = unicode(payload, charset, 'replace') except (LookupError, TypeError): # Unknown or empty charset. payload = unicode(payload, 'us-ascii', 'replace') print >> self._text, payload if not payload.endswith('\n'): print >> self._text
def wrap_message(mlist, msg, msgdata): # Create a wrapper message around the original. # # There are various headers in msg that we don't want, so we basically # make a copy of the message, then delete almost everything and set/copy # what we want. original_msg = copy.deepcopy(msg) for key in msg: keep = False for keeper in KEEPERS: if re.match(keeper, key, re.IGNORECASE): keep = True break if not keep: del msg[key] msg['MIME-Version'] = '1.0' msg['Message-ID'] = make_msgid() for key, value in munged_headers(mlist, original_msg, msgdata): msg[key] = value # Are we including dmarc_wrapped_message_text? if len(mlist.dmarc_wrapped_message_text) > 0: part1 = MIMEText(wrap(mlist.dmarc_wrapped_message_text), 'plain', mlist.preferred_language.charset) part1['Content-Disposition'] = 'inline' part2 = MIMEMessage(original_msg) part2['Content-Disposition'] = 'inline' msg['Content-Type'] = 'multipart/mixed' msg.set_payload([part1, part2]) else: msg['Content-Type'] = 'message/rfc822' msg['Content-Disposition'] = 'inline' msg.set_payload([original_msg]) return
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. template = getUtility(ITemplateLoader).get( 'list:member:digest:masthead', mlist) self._masthead = wrap(expand(template, mlist, dict( # For backward compatibility. got_list_email=mlist.posting_address, got_request_email=mlist.request_address, got_owner_email=mlist.owner_address, ))) # Set things up for the table of contents. self._header = decorate('list:member:digest:header', mlist) self._toc = StringIO() print(_("Today's Topics:\n"), file=self._toc)
def test_dont_honor_ws(self): text = """\ This is a single paragraph. It consists of several sentences none of which are very long. This paragraph is indented but we don't honor whitespace so it will be filled. And here is a second paragraph which also consists of several sentences. None of these are very long either. """ self.assertEqual( wrap(text, honor_leading_ws=False), """\ This is a single paragraph. It consists of several sentences none of which are very long. This paragraph is indented but we don't honor whitespace so it will be filled. And here is a second paragraph which also consists of several sentences. None of these are very long either.""")
def test_honor_ws(self): text = """\ This is a single paragraph. It consists of several sentences none of which are very long. This paragraph is indented so it won't be filled. And here is a second paragraph which also consists of several sentences. None of these are very long either. """ self.assertEqual( wrap(text), """\ This is a single paragraph. It consists of several sentences none of which are very long. This paragraph is indented so it won't be filled. And here is a second paragraph which also consists of several sentences. None of these are very long either.""", )
def add_to_toc(self, msg, count): """Add a message to the table of contents.""" subject = msg.get('subject', _('(no subject)')) subject = oneline(subject, in_unicode=True) # Don't include the redundant subject prefix in the toc mo = re.match('(re:? *)?({0})'.format( re.escape(self._mlist.subject_prefix)), subject, re.IGNORECASE) if mo: subject = subject[:mo.start(2)] + subject[mo.end(2):] # Take only the first author we find. username = '' addresses = getaddresses( [oneline(msg.get('from', ''), in_unicode=True)]) if addresses: username = addresses[0][0] if not username: username = addresses[0][1] if username: username = '******'.format(username) lines = wrap('{0:2}. {1}'. format(count, subject), 65).split('\n') # See if the user's name can fit on the last line if len(lines[-1]) + len(username) > 70: lines.append(username) else: lines[-1] += username # Add this subject to the accumulating topics first = True for line in lines: if first: print(' ', line, file=self._toc) first = False else: print(' ', line.lstrip(), file=self._toc)
def add_message(self, msg, count): """Add the message to the digest.""" if count > 1: print(self._separator30, file=self._text) print(file=self._text) # Each message section contains a few headers. for header in config.digests.plain_digest_keep_headers.split(): if header in msg: value = oneline(msg[header], in_unicode=True) value = wrap('{0}: {1}'.format(header, value)) value = '\n\t'.join(value.split('\n')) print(value, file=self._text) print(file=self._text) # Add the payload. If the decoded payload is empty, this may be a # multipart message. In that case, just stringify it. payload = msg.get_payload(decode=True) if not payload: payload = msg.as_string().split('\n\n', 1)[1] if isinstance(payload, bytes): try: # Do the decoding inside the try/except so that if the charset # conversion fails, we'll just drop back to ascii. charset = msg.get_content_charset('us-ascii') payload = payload.decode(charset, 'replace') except (LookupError, TypeError): # Unknown or empty charset. payload = payload.decode('us-ascii', 'replace') print(payload, file=self._text) if not payload.endswith('\n'): print(file=self._text)
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)
def hold_unsubscription(mlist, email): data = dict(email=email) requestsdb = IListRequests(mlist) request_id = requestsdb.hold_request(RequestType.unsubscription, email, data) vlog.info('%s: held unsubscription request from %s', mlist.fqdn_listname, email) # Possibly notify the administrator of the hold if mlist.admin_immed_notify: subject = _( 'New unsubscription request from $mlist.display_name by $email') template = getUtility(ITemplateLoader).get( 'list:admin:action:unsubscribe', mlist) text = wrap( expand( template, mlist, dict( # For backward compatibility. mailing_list=mlist, member=email, email=email, ))) # This message should appear to come from the <list>-owner so as # to avoid any useless bounce processing. msg = UserNotification(mlist.owner_address, mlist.owner_address, subject, text, mlist.preferred_language) msg.send(mlist) return request_id
def process(self, mlist, msg, msgdata, arguments, results): """See `IEmailCommand`.""" # With no argument, print the command and a short description, which # is contained in the short_description attribute. if len(arguments) == 0: length = max(len(command) for command in config.commands) format = '{{0: <{0}s}} - {{1}}'.format(length) for command_name in sorted(config.commands): command = config.commands[command_name] short_description = getattr(command, 'short_description', _('n/a')) print(format.format(command.name, short_description), file=results) return ContinueProcessing.yes elif len(arguments) == 1: command_name = arguments[0] command = config.commands.get(command_name) if command is None: print(_('$self.name: no such command: $command_name'), file=results) return ContinueProcessing.no print('{} {}'.format(command.name, command.argument_description), file=results) print(command.short_description, file=results) if command.short_description != command.description: print(wrap(command.description), file=results) return ContinueProcessing.yes else: printable_arguments = SPACE.join(arguments) # noqa: F841 print(_('$self.name: too many arguments: $printable_arguments'), file=results) return ContinueProcessing.no
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)) template = getUtility(ITemplateLoader).get( 'list:admin:action:subscribe', self.mlist) text = wrap(expand(template, self.mlist, dict(member=username, ))) # This message should appear to come from the <list>-owner so as # to avoid any useless bounce processing. msg = UserNotification(self.mlist.owner_address, self.mlist.owner_address, subject, text, self.mlist.preferred_language) msg.send(self.mlist) # The workflow must stop running here. raise StopIteration
def process(self, mlist, msg, msgdata, arguments, results): """See `IEmailCommand`.""" # With no argument, print the command and a short description, which # is contained in the short_description attribute. if len(arguments) == 0: length = max(len(command) for command in config.commands) format = '{{0: <{0}s}} - {{1}}'.format(length) for command_name in sorted(config.commands): command = config.commands[command_name] short_description = getattr( command, 'short_description', _('n/a')) print(format.format(command.name, short_description), file=results) return ContinueProcessing.yes elif len(arguments) == 1: command_name = arguments[0] command = config.commands.get(command_name) if command is None: print(_('$self.name: no such command: $command_name'), file=results) return ContinueProcessing.no print('{0} {1}'.format(command.name, command.argument_description), file=results) print(command.short_description, file=results) if command.short_description != command.description: print(wrap(command.description), file=results) return ContinueProcessing.yes else: printable_arguments = SPACE.join(arguments) print(_('$self.name: too many arguments: $printable_arguments'), file=results) return ContinueProcessing.no
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)
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) 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.subscriber.email, # For backward compatibility. address=member.address.email, email=member.address.email, 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_welcome_message(mlist, address, language, delivery_mode, text=''): """Send a welcome message to a subscriber. Prepending to the standard welcome message template is the mailing list's welcome message, if there is one. :param mlist: the mailing list :type mlist: IMailingList :param address: The address to respond to :type address: string :param language: the language of the response :type language: ILanguage :param delivery_mode: the type of delivery the subscriber is getting :type delivery_mode: DeliveryMode """ if mlist.welcome_message_uri: try: uri = expand(mlist.welcome_message_uri, dict( listname=mlist.fqdn_listname, language=language.code, )) welcome_message = getUtility(ITemplateLoader).get(uri) except URLError: log.exception('Welcome message URI not found ({0}): {1}'.format( mlist.fqdn_listname, mlist.welcome_message_uri)) welcome = '' else: welcome = wrap(welcome_message) else: welcome = '' # Find the IMember object which is subscribed to the mailing list, because # from there, we can get the member's options url. member = mlist.members.get_member(address) user_name = member.user.display_name options_url = member.options_url # Get the text from the template. text = expand(welcome, dict( fqdn_listname=mlist.fqdn_listname, list_name=mlist.display_name, listinfo_uri=mlist.script_url('listinfo'), list_requests=mlist.request_address, user_name=user_name, user_address=address, user_options_uri=options_url, )) if delivery_mode is not DeliveryMode.regular: digmode = _(' (Digest mode)') else: digmode = '' msg = UserNotification( formataddr((user_name, address)), mlist.request_address, _('Welcome to the "$mlist.display_name" mailing list${digmode}'), text, language) msg['X-No-Archive'] = 'yes' msg.send(mlist, verp=as_boolean(config.mta.verp_personalized_deliveries))
def test_simple_wrap(self): text = """\ This is a single paragraph. It consists of several sentences none of which are very long. """ self.assertEqual(wrap(text), """\ This is a single paragraph. It consists of several sentences none of which are very long.""")
def process(self, mlist, msg, msgdata): """See `IHandler`.""" # Short circuit if we've already calculated the recipients list, # regardless of whether the list is empty or not. if 'recipients' in msgdata: return # Should the original sender should be included in the recipients list? include_sender = True member = mlist.members.get_member(msg.sender) if member and not member.receive_own_postings: include_sender = False # Support for urgent messages, which bypasses digests and disabled # delivery and forces an immediate delivery to all members Right Now. # We are specifically /not/ allowing the site admins password to work # here because we want to discourage the practice of sending the site # admin password through email in the clear. (see also Approve.py) # # XXX This is broken. missing = object() password = msg.get('urgent', missing) if password is not missing: if mlist.Authenticate((config.AuthListModerator, config.AuthListAdmin), password): recipients = mlist.getMemberCPAddresses( mlist.getRegularMemberKeys() + mlist.getDigestMemberKeys()) msgdata['recipients'] = recipients return else: # Bad Urgent: password, so reject it instead of passing it on. # I think it's better that the sender know they screwed up # than to deliver it normally. text = _("""\ Your urgent message to the $mlist.display_name mailing list was not authorized for delivery. The original message as received by Mailman is attached. """) raise RejectMessage(wrap(text)) # Calculate the regular recipients of the message recipients = set(member.address.email for member in mlist.regular_members.members if member.delivery_status == DeliveryStatus.enabled) # Remove the sender if they don't want to receive their own posts if not include_sender and member.address.email in recipients: recipients.remove(member.address.email) # Handle topic classifications # XXX: Disabled for now until we fix it properly # # do_topic_filters(mlist, msg, msgdata, recipients) # # Bookkeeping msgdata['recipients'] = recipients
def test_indentation_boundary(self): text = """\ This is a single paragraph that consists of one sentence. And another one that breaks because it is indented. Followed by one more paragraph. """ self.assertEqual(wrap(text), """\ This is a single paragraph that consists of one sentence. And another one that breaks because it is indented. Followed by one more paragraph.""")
def check(self, mlist, msg, msgdata): """See `IRule`.""" if not as_boolean(config.mailman.hold_digest): return False # Convert the header value to a str because it may be an # email.header.Header instance. subject = str(msg.get('subject', '')).strip() if DIGRE.search(subject): msgdata['moderation_sender'] = msg.sender with _.defer_translation(): # This will be translated at the point of use. msgdata.setdefault('moderation_reasons', []).append( _('Message has a digest subject')) return True # Get the masthead, but without emails. mastheadtxt = getUtility(ITemplateLoader).get( 'list:member:digest:masthead', mlist) mastheadtxt = wrap( expand( mastheadtxt, mlist, dict( display_name=mlist.display_name, listname='', list_id=mlist.list_id, request_email='', owner_email='', ))) msgtext = '' for part in msg.walk(): if part.get_content_maintype() == 'text': cset = part.get_content_charset('utf-8') msgtext += part.get_payload(decode=True).decode( cset, errors='replace') matches = 0 lines = mastheadtxt.splitlines() for line in lines: line = line.strip() if not line: continue if msgtext.find(line) >= 0: matches += 1 if matches >= int(config.mailman.masthead_threshold): msgdata['moderation_sender'] = msg.sender with _.defer_translation(): # This will be translated at the point of use. msgdata.setdefault('moderation_reasons', []).append( _('Message quotes digest boilerplate')) return True return False
def _get_message(uri_template, mlist, language): if not uri_template: return '' try: uri = expand(uri_template, dict( listname=mlist.fqdn_listname, language=language.code, )) message = getUtility(ITemplateLoader).get(uri) except URLError: log.exception('Message URI not found ({0}): {1}'.format( mlist.fqdn_listname, uri_template)) return '' else: return wrap(message)
def _get_message(uri_template, mlist, language): if not uri_template: return '' try: uri = expand( uri_template, dict( listname=mlist.fqdn_listname, language=language.code, )) message = getUtility(ITemplateLoader).get(uri) except URLError: log.exception('Message URI not found ({0}): {1}'.format( mlist.fqdn_listname, uri_template)) return '' else: return wrap(message)
def send_welcome_message(mlist, member, language, text=''): """Send a welcome message to a subscriber. Prepending to the standard welcome message template is the mailing list's welcome message, if there is one. :param mlist: The mailing list. :type mlist: IMailingList :param member: The member to send the welcome message to. :param address: IMember :param language: The language of the response. :type language: ILanguage """ welcome_message = wrap( getUtility(ITemplateLoader).get('list:user:notice:welcome', mlist, language=language.code)) display_name = member.display_name # Get the text from the template. text = expand( welcome_message, mlist, dict( user_name=display_name, user_email=member.address.email, # For backward compatibility. user_address=member.address.email, fqdn_listname=mlist.fqdn_listname, list_name=mlist.display_name, list_requests=mlist.request_address, )) digmode = ( '' # noqa: F841 if member.delivery_mode is DeliveryMode.regular else _(' (Digest mode)')) msg = UserNotification( formataddr( (display_name, member.address.email)), mlist.request_address, _('Welcome to the "$mlist.display_name" mailing list${digmode}'), text, language) msg['X-No-Archive'] = 'yes' msg.send(mlist, verp=as_boolean(config.mta.verp_personalized_deliveries))
def test_two_paragraphs(self): text = """\ This is a single paragraph. It consists of several sentences none of which are very long. And here is a second paragraph which also consists of several sentences. None of these are very long either. """ self.assertEqual(wrap(text), """\ This is a single paragraph. It consists of several sentences none of which are very long. And here is a second paragraph which also consists of several sentences. None of these are very long either.""")
def send_user_disable_warning(mlist, address, language): """Sends a warning mail to the user reminding the person to reenable its DeliveryStatus. :param mlist: The mailing list :type mlist: IMailingList :param address: The address of the member :type address: string. :param language: member's preferred language :type language: ILanguage """ warning_message = wrap( getUtility(ITemplateLoader).get('list:user:notice:warning', mlist, language=language.code)) warning_message_text = expand(warning_message, mlist, dict(sender_email=address)) msg = UserNotification( address, mlist.bounces_address, _('Your subscription for ${mlist.display_name} mailing list' ' has been disabled'), warning_message_text, language) msg.send(mlist, verp=as_boolean(config.mta.verp_personalized_deliveries))
def send_goodbye_message(mlist, address, language): """Send a goodbye message to a subscriber. Prepending to the standard goodbye message template is the mailing list's goodbye message, if there is one. :param mlist: the mailing list :type mlist: IMailingList :param address: The address to respond to :type address: string :param language: the language of the response :type language: ILanguage """ goodbye_message = wrap( getUtility(ITemplateLoader).get('list:user:notice:goodbye', mlist, language=language.code)) msg = UserNotification( address, mlist.bounces_address, _('You have been unsubscribed from the $mlist.display_name ' 'mailing list'), goodbye_message, language) msg.send(mlist, verp=as_boolean(config.mta.verp_personalized_deliveries))
def _step_get_moderator_approval(self): self._set_token(TokenOwner.moderator) self.push('unsubscribe_from_restored') self.save() log.info('{}: held unsubscription request from {}'.format( self.mlist.fqdn_listname, self.address.email)) if self.mlist.admin_immed_notify: subject = _( 'New unsubscription request to $self.mlist.display_name ' 'from $self.address.email') username = formataddr( (self.subscriber.display_name, self.address.email)) template = getUtility(ITemplateLoader).get( 'list:admin:action:unsubscribe', self.mlist) text = wrap(expand(template, self.mlist, dict(member=username, ))) # This message should appear to come from the <list>-owner so as # to avoid any useless bounce processing. msg = UserNotification(self.mlist.owner_address, self.mlist.owner_address, subject, text, self.mlist.preferred_language) msg.send(self.mlist, tomoderators=True) # The workflow must stop running here raise StopIteration
def send_goodbye_message(mlist, address, language): """Send a goodbye message to a subscriber. Prepending to the standard goodbye message template is the mailing list's goodbye message, if there is one. :param mlist: the mailing list :type mlist: IMailingList :param address: The address to respond to :type address: string :param language: the language of the response :type language: string """ if mlist.goodbye_msg: goodbye = wrap(mlist.goodbye_msg) + '\n' else: goodbye = '' msg = UserNotification( address, mlist.bounces_address, _('You have been unsubscribed from the $mlist.display_name ' 'mailing list'), goodbye, language) msg.send(mlist, verp=as_boolean(config.mta.verp_personalized_deliveries))
def add_message(self, msg, count): """Add the message to the digest.""" if count > 1: print(self._separator30, file=self._text) print(file=self._text) # Each message section contains a few headers. # add the Message: n header first. print('Message: {}'.format(count), file=self._text) # Then the others. for header in config.digests.plain_digest_keep_headers.split(): if header in msg: value = oneline(msg[header], in_unicode=True) value = wrap('{}: {}'.format(header, value)) value = '\n\t'.join(value.split('\n')) print(value, file=self._text) print(file=self._text) # Get the scrubbed payload. This is the original payload with all # non text/plain parts replaced by notes that they've been removed. payload = scrub(msg) # Add the payload. print(payload, file=self._text) if not payload.endswith('\n'): print(file=self._text)
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 do_list_categories(mlist, k, subcat, outfp): info = mlist.GetConfigInfo(k, subcat) label, gui = mlist.GetConfigCategories()[k] if info is None: return charset = mlist.preferred_language.charset print >> outfp, '##', k.capitalize(), _('options') print >> outfp, '#' # First, massage the descripton text, which could have obnoxious # leading whitespace on second and subsequent lines due to # triple-quoted string nonsense in the source code. desc = NL.join([s.lstrip() for s in info[0].splitlines()]) # Print out the category description desc = wrap(desc) for line in desc.splitlines(): print >> outfp, '#', line print >> outfp for data in info[1:]: if not isinstance(data, tuple): continue varname = data[0] # Variable could be volatile if varname[0] == '_': continue vtype = data[1] # First, massage the descripton text, which could have # obnoxious leading whitespace on second and subsequent lines # due to triple-quoted string nonsense in the source code. desc = NL.join([s.lstrip() for s in data[-1].splitlines()]) # Now strip out all HTML tags desc = re.sub('<.*?>', '', desc) # And convert </> to <> desc = re.sub('<', '<', desc) desc = re.sub('>', '>', desc) # Print out the variable description. desc = wrap(desc) for line in desc.split('\n'): print >> outfp, '#', line # munge the value based on its type value = None if hasattr(gui, 'getValue'): value = gui.getValue(mlist, vtype, varname, data[2]) if value is None and not varname.startswith('_'): value = getattr(mlist, varname) if vtype in (config.String, config.Text, config.FileUpload): print >> outfp, varname, '=', lines = value.splitlines() if not lines: print >> outfp, "''" elif len(lines) == 1: if charset != 'us-ascii' and nonasciipat.search(lines[0]): # This is more readable for non-english list. print >> outfp, '"' + lines[0].replace('"', '\\"') + '"' else: print >> outfp, repr(lines[0]) else: if charset == 'us-ascii' and nonasciipat.search(value): # Normally, an english list should not have non-ascii char. print >> outfp, repr(NL.join(lines)) else: outfp.write(' """') outfp.write(NL.join(lines).replace('"', '\\"')) outfp.write('"""\n') elif vtype in (config.Radio, config.Toggle): print >> outfp, '#' print >> outfp, '#', _('legal values are:') # TBD: This is disgusting, but it's special cased # everywhere else anyway... if varname == 'subscribe_policy' and \ not config.ALLOW_OPEN_SUBSCRIBE: i = 1 else: i = 0 for choice in data[2]: print >> outfp, '# ', i, '= "%s"' % choice i += 1 print >> outfp, varname, '=', repr(value) else: print >> outfp, varname, '=', repr(value) print >> outfp
def process(self, mlist, msg, msgdata): """See `IHandler`.""" # There are several cases where the replybot is short-circuited: # * the original message has an "X-Ack: No" header # * the message has a Precedence header with values bulk, junk, or # list, and there's no explicit "X-Ack: yes" header # * the message metadata has a true 'noack' key ack = msg.get('x-ack', '').lower() if ack == 'no' or msgdata.get('noack'): return precedence = msg.get('precedence', '').lower() if ack != 'yes' and precedence in ('bulk', 'junk', 'list'): return # Check to see if the list is even configured to autorespond to this # email message. Note: the incoming message processors should set the # destination key in the message data. if msgdata.get('to_owner'): if mlist.autorespond_owner is ResponseAction.none: return response_type = Response.owner response_text = mlist.autoresponse_owner_text elif msgdata.get('to_request'): if mlist.autorespond_requests is ResponseAction.none: return response_type = Response.command response_text = mlist.autoresponse_request_text elif msgdata.get('to_list'): if mlist.autorespond_postings is ResponseAction.none: return response_type = Response.postings response_text = mlist.autoresponse_postings_text else: # There are no automatic responses for any other destination. return # Now see if we're in the grace period for this sender. grace_period # = 0 means always automatically respond, as does an "X-Ack: yes" # header (useful for debugging). response_set = IAutoResponseSet(mlist) user_manager = getUtility(IUserManager) address = user_manager.get_address(msg.sender) if address is None: address = user_manager.create_address(msg.sender) grace_period = mlist.autoresponse_grace_period if grace_period > ALWAYS_REPLY and ack != 'yes': last = response_set.last_response(address, response_type) if last is not None and last.date_sent + grace_period > today(): return # Okay, we know we're going to respond to this sender, craft the # message, send it, and update the database. display_name = mlist.display_name subject = _( 'Auto-response for your message to the "$display_name" ' 'mailing list') # Do string interpolation into the autoresponse text d = dict(list_name = mlist.list_name, display_name = display_name, listurl = mlist.script_url('listinfo'), requestemail = mlist.request_address, owneremail = mlist.owner_address, ) # Interpolation and Wrap the response text. text = wrap(expand(response_text, d)) outmsg = UserNotification(msg.sender, mlist.bounces_address, subject, text, mlist.preferred_language) outmsg['X-Mailer'] = _('The Mailman Replybot') # prevent recursions and mail loops! outmsg['X-Ack'] = 'No' outmsg.send(mlist) response_set.response_sent(address, response_type)
def process(self, mlist, msg, msgdata): """See `IHandler`.""" # There are several cases where the replybot is short-circuited: # * the original message has an "X-Ack: No" header # * the message has a Precedence header with values bulk, junk, or # list, and there's no explicit "X-Ack: yes" header # * the message metadata has a true 'noack' key ack = msg.get('x-ack', '').lower() if ack == 'no' or msgdata.get('noack'): return precedence = msg.get('precedence', '').lower() if ack != 'yes' and precedence in ('bulk', 'junk', 'list'): return # Check to see if the list is even configured to autorespond to this # email message. Note: the incoming message processors should set the # destination key in the message data. if msgdata.get('to_owner'): if mlist.autorespond_owner is ResponseAction.none: return response_type = Response.owner response_text = mlist.autoresponse_owner_text elif msgdata.get('to_request'): if mlist.autorespond_requests is ResponseAction.none: return response_type = Response.command response_text = mlist.autoresponse_request_text elif msgdata.get('to_list'): if mlist.autorespond_postings is ResponseAction.none: return response_type = Response.postings response_text = mlist.autoresponse_postings_text else: # There are no automatic responses for any other destination. return # Now see if we're in the grace period for this sender. grace_period # = 0 means always automatically respond, as does an "X-Ack: yes" # header (useful for debugging). response_set = IAutoResponseSet(mlist) user_manager = getUtility(IUserManager) address = user_manager.get_address(msg.sender) if address is None: address = user_manager.create_address(msg.sender) grace_period = mlist.autoresponse_grace_period if grace_period > ALWAYS_REPLY and ack != 'yes': last = response_set.last_response(address, response_type) if last is not None and last.date_sent + grace_period > today(): return # Okay, we know we're going to respond to this sender, craft the # message, send it, and update the database. display_name = mlist.display_name subject = _('Auto-response for your message to the "$display_name" ' 'mailing list') # Do string interpolation into the autoresponse text d = dict( list_name=mlist.list_name, display_name=display_name, requestemail=mlist.request_address, owneremail=mlist.owner_address, ) # Interpolation and Wrap the response text. text = wrap(expand(response_text, mlist, d)) outmsg = UserNotification(msg.sender, mlist.bounces_address, subject, text, mlist.preferred_language) outmsg['X-Mailer'] = _('The Mailman Replybot') # prevent recursions and mail loops! outmsg['X-Ack'] = 'No' outmsg.send(mlist) response_set.response_sent(address, response_type)
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))
def _compose_reasons(msgdata, column=66): # Rules can add reasons to the metadata. reasons = msgdata.get('moderation_reasons', [_('N/A')]) return NL.join( [(SPACE * 4) + wrap(_(reason), column=column) for reason in reasons])
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. template = getUtility(ITemplateLoader).get( 'list:user:notice:no-more-today', mlist, language=language.code) text = wrap(expand(template, mlist, dict( language=language.code, count=todays_count, sender_email=sender, # For backward compatibility. sender=sender, 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_wrap_blank_paragraph(self): self.assertEqual(string.wrap('\n\n'), '\n\n')