def process(self, mlist, msg, msgdata, arguments, results): """See `IEmailCommand`.""" # Parse the arguments. delivery_mode = self._parse_arguments(arguments, results) if delivery_mode is ContinueProcessing.no: return ContinueProcessing.no display_name, address = parseaddr(msg['from']) # Address could be None or the empty string. if not address: address = msg.sender if not address: print(_('$self.name: No valid address found to subscribe'), file=results) return ContinueProcessing.no # Have we already seen one join request from this user during the # processing of this email? joins = getattr(results, 'joins', set()) if address in joins: # Do not register this join. return ContinueProcessing.yes joins.add(address) results.joins = joins person = formataddr((display_name, address)) # Is this person already a member of the list? Search for all # matching memberships. members = getUtility(ISubscriptionService).find_members( address, mlist.list_id, MemberRole.member) if len(members) > 0: print(_('$person is already a member'), file=results) else: getUtility(IRegistrar).register(mlist, address, display_name, delivery_mode) print(_('Confirmation email sent to $person'), file=results) return ContinueProcessing.yes
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 process(self, mlist, msg, msgdata, arguments, results): """See `IEmailCommand`.""" # The token must be in the arguments. if len(arguments) == 0: print(_('No confirmation token found'), file=results) return ContinueProcessing.no # Make sure we don't try to confirm the same token more than once. token = arguments[0] tokens = getattr(results, 'confirms', set()) if token in tokens: # Do not try to confirm this one again. return ContinueProcessing.yes tokens.add(token) results.confirms = tokens try: token, token_owner, member = IRegistrar(mlist).confirm(token) if token is None: assert token_owner is TokenOwner.no_one, token_owner assert member is not None, member succeeded = True else: assert token_owner is not TokenOwner.no_one, token_owner assert member is None, member succeeded = False except LookupError: # The token must not exist in the database. succeeded = False if succeeded: print(_('Confirmed'), file=results) return ContinueProcessing.yes print(_('Confirmation token did not match'), file=results) return ContinueProcessing.no
def add(self, parser, command_parser): """See `ICLISubCommand`.""" self.parser = parser command_parser.add_argument( '-q', '--queue', type=unicode, help=_(""" The name of the queue to inject the message to. QUEUE must be one of the directories inside the qfiles directory. If omitted, the incoming queue is used.""")) command_parser.add_argument( '-s', '--show', action='store_true', default=False, help=_('Show a list of all available queue names and exit.')) command_parser.add_argument( '-f', '--filename', type=unicode, help=_(""" Name of file containing the message to inject. If not given, or '-' (without the quotes) standard input is used.""")) # Required positional argument. command_parser.add_argument( 'listname', metavar='LISTNAME', nargs=1, help=_(""" The 'fully qualified list name', i.e. the posting address of the mailing list to inject the message into.""")) command_parser.add_argument( '-m', '--metadata', dest='keywords', action='append', default=[], metavar='KEY=VALUE', help=_(""" Additional metadata key/value pairs to add to the message metadata dictionary. Use the format key=value. Multiple -m options are allowed."""))
def handle_unsubscription(mlist, id, action, comment=None): requestdb = IListRequests(mlist) key, data = requestdb.get_request(id) address = data['address'] if action is Action.defer: # Nothing to do. return elif action is Action.discard: # Nothing to do except delete the request from the database. pass elif action is Action.reject: key, data = requestdb.get_request(id) _refuse(mlist, _('Unsubscription request'), address, comment or _('[No reason given]')) elif action is Action.accept: key, data = requestdb.get_request(id) try: delete_member(mlist, address) except NotAMemberError: # User has already been unsubscribed. pass slog.info('%s: deleted %s', mlist.fqdn_listname, address) else: raise AssertionError('Unexpected action: {0}'.format(action)) # Delete the request from the database. requestdb.delete_request(id)
def add(self, parser, command_parser): """See `ICLISubCommand`.""" command_parser.add_argument( '-a', '--advertised', default=False, action='store_true', help=_( 'List only those mailing lists that are publicly advertised')) command_parser.add_argument( '-n', '--names', default=False, action='store_true', help=_('Show also the list names')) command_parser.add_argument( '-d', '--descriptions', default=False, action='store_true', help=_('Show also the list descriptions')) command_parser.add_argument( '-q', '--quiet', default=False, action='store_true', help=_('Less verbosity')) command_parser.add_argument( '--domain', action='append', help=_("""\ List only those mailing lists hosted on the given domain, which must be the email host name. Multiple -d options may be given. """))
def _parse_arguments(self, arguments, results): """Parse command arguments. :param arguments: The sequences of arguments as given to the `process()` method. :param results: The results object. :return: The delivery mode, None, or ContinueProcessing.no on error. """ mode = DeliveryMode.regular for argument in arguments: parts = argument.split('=', 1) if len(parts) != 2 or parts[0] != 'digest': print(self.name, _('bad argument: $argument'), file=results) return ContinueProcessing.no mode = { 'no': DeliveryMode.regular, 'plain': DeliveryMode.plaintext_digests, 'mime': DeliveryMode.mime_digests, }.get(parts[1]) if mode is None: print(self.name, _('bad argument: $argument'), file=results) return ContinueProcessing.no return mode
def finish(self): """Finish up the digest, producing the email-ready copy.""" if self._mlist.digest_footer_uri is not None: try: footer_text = decorate(self._mlist, self._mlist.digest_footer_uri) except URLError: log.exception( "Digest footer decorator URI not found ({0}): {1}".format( self._mlist.fqdn_listname, self._mlist.digest_footer_uri ) ) footer_text = "" # MAS: There is no real place for the digest_footer in an RFC 1153 # compliant digest, so add it as an additional message with # Subject: Digest Footer print >>self._text, self._separator30 print >>self._text print >>self._text, "Subject: " + _("Digest Footer") print >>self._text print >>self._text, footer_text print >>self._text print >>self._text, self._separator30 print >>self._text # Add the sign-off. sign_off = _("End of ") + self._digest_id print >>self._text, sign_off print >>self._text, "*" * len(sign_off) # If the digest message can't be encoded by the list character set, # fall back to utf-8. text = self._text.getvalue() try: self._message.set_payload(text.encode(self._charset), charset=self._charset) except UnicodeError: self._message.set_payload(text.encode("utf-8"), charset="utf-8") return self._message
def add(self, parser, command_parser): """See `ICLISubCommand`.""" self.parser = parser command_parser.add_argument( '-i', '--interactive', default=None, action='store_true', help=_("""\ Leaves you at an interactive prompt after all other processing is complete. This is the default unless the --run option is given.""")) command_parser.add_argument( '-r', '--run', help=_("""\ Run a script on a mailing list. The argument is the module path to a callable. This callable will be imported and then called with the mailing list as the first argument. If additional arguments are given at the end of the command line, they are passed as subsequent positional arguments to the callable. For additional help, see --details. """)) command_parser.add_argument( '--details', default=False, action='store_true', help=_('Print detailed instructions on using this command.')) # Optional positional argument. command_parser.add_argument( 'listname', metavar='LISTNAME', nargs='?', help=_("""\ The 'fully qualified list name', i.e. the posting address of the mailing list to inject the message into. This can be a Python regular expression, in which case all mailing lists whose posting address matches will be processed. To use a regular expression, LISTNAME must start with a ^ (and the matching is done with re.match(). LISTNAME cannot be a regular expression unless --run is given."""))
def add(self, parser, command_parser): """See `ICLISubCommand`.""" self.parser = parser command_parser.add_argument( '-o', '--output', action='store', help=_("""\ File to send the output to. If not given, or if '-' is given, standard output is used.""")) command_parser.add_argument( '-s', '--section', action='store', help=_("""\ Section to use for the lookup. If no key is given, all the key-value pairs of the given section will be displayed. """)) command_parser.add_argument( '-k', '--key', action='store', help=_("""\ Key to use for the lookup. If no section is given, all the key-values pair from any section matching the given key will be displayed. """)) command_parser.add_argument( '-t', '--sort', default=False, action='store_true', help=_('Sort the output by sections and keys.'))
def add_options(self): """See `Options`.""" self.parser.add_option( '-n', '--no-restart', dest='restartable', default=True, action='store_false', help=_("""\ Don't restart the runners when they exit because of an error or a SIGUSR1. Use this only for debugging.""")) self.parser.add_option( '-f', '--force', default=False, action='store_true', help=_("""\ If the master watcher finds an existing master lock, it will normally exit with an error message. With this option,the master will perform an extra level of checking. If a process matching the host/pid described in the lock file is running, the master will still exit, requiring you to manually clean up the lock. But if no matching process is found, the master will remove the apparently stale lock and make another attempt to claim the master lock.""")) self.parser.add_option( '-r', '--runner', dest='runners', action='append', default=[], help=_("""\ Override the default set of runners that the master will invoke, which is typically defined in the configuration file. Multiple -r options may be given. The values for -r are passed straight through to bin/runner."""))
def parseargs(): parser = optparse.OptionParser( version=MAILMAN_VERSION, usage=_( """\ %prog [options] [listname ...] List the owners of a mailing list, or all mailing lists if no list names are given.""" ), ) parser.add_option( "-w", "--with-listnames", default=False, action="store_true", help=_( """\ Group the owners by list names and include the list names in the output. Otherwise, the owners will be sorted and uniquified based on the email address.""" ), ) parser.add_option( "-m", "--moderators", default=False, action="store_true", help=_("Include the list moderators in the output.") ) parser.add_option("-C", "--config", help=_("Alternative configuration file to use")) opts, args = parser.parse_args() return parser, opts, args
def dispose(mlist, msg, msgdata, why): if mlist.filter_action is FilterAction.reject: # Bounce the message to the original author. raise errors.RejectMessage(why) elif mlist.filter_action is FilterAction.forward: # Forward it on to the list moderators. text=_("""\ The attached message matched the $mlist.display_name mailing list's content filtering rules and was prevented from being forwarded on to the list membership. You are receiving the only remaining copy of the discarded message. """) subject=_('Content filter message notification') notice = OwnerNotification(mlist, subject, roster=mlist.moderators) notice.set_type('multipart/mixed') notice.attach(MIMEText(text)) notice.attach(MIMEMessage(msg)) notice.send(mlist) # Let this fall through so the original message gets discarded. elif mlist.filter_action is FilterAction.preserve: if as_boolean(config.mailman.filtered_messages_are_preservable): # This is just like discarding the message except that a copy is # placed in the 'bad' queue should the site administrator want to # inspect the message. filebase = config.switchboards['bad'].enqueue(msg, msgdata) log.info('{0} preserved in file base {1}'.format( msg.get('message-id', 'n/a'), filebase)) else: log.error( '{1} invalid FilterAction: {0}. Treating as discard'.format( mlist.fqdn_listname, mlist.filter_action.name)) # Most cases also discard the message raise errors.DiscardMessage(why)
def process(self, args): """See `ICLISubCommand`.""" # Although there's a potential race condition here, it's a better user # experience for the parent process to refuse to start twice, rather # than having it try to start the master, which will error exit. status, lock = master_state() if status is WatcherState.conflict: self.parser.error(_('GNU Mailman is already running')) elif status in (WatcherState.stale_lock, WatcherState.host_mismatch): if args.force is None: self.parser.error( _('A previous run of GNU Mailman did not exit ' 'cleanly. Try using --force.')) def log(message): # noqa if not args.quiet: print(message) # Try to find the path to a valid, existing configuration file, and # refuse to start if one cannot be found. if args.config is not None: config_path = args.config elif config.filename is not None: config_path = config.filename else: config_path = os.path.join(config.VAR_DIR, 'etc', 'mailman.cfg') if not os.path.exists(config_path): print(_("""\ No valid configuration file could be found, so Mailman will refuse to start. Use -C/--config to specify a valid configuration file."""), file=sys.stderr) sys.exit(1) # Daemon process startup according to Stevens, Advanced Programming in # the UNIX Environment, Chapter 13. pid = os.fork() if pid: # parent log(_("Starting Mailman's master runner")) return # child: Create a new session and become the session leader, but since # we won't be opening any terminal devices, don't do the # ultra-paranoid suggestion of doing a second fork after the setsid() # call. os.setsid() # Instead of cd'ing to root, cd to the Mailman runtime directory. # However, before we do that, set an environment variable used by the # subprocesses to calculate their path to the $VAR_DIR. os.environ['MAILMAN_VAR_DIR'] = config.VAR_DIR os.chdir(config.VAR_DIR) # Exec the master watcher. execl_args = [ sys.executable, sys.executable, os.path.join(config.BIN_DIR, 'master'), ] if args.force: execl_args.append('--force') # Always pass the config file path to the master projects, so there's # no confusion about which cfg is being used. execl_args.extend(['-C', config_path]) qlog.debug('starting: %s', execl_args) os.execl(*execl_args) # We should never get here. raise RuntimeError('os.execl() failed')
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 bounce_message(mlist, msg, error=None): """Bounce the message back to the original author. :param mlist: The mailing list that the message was posted to. :type mlist: `IMailingList` :param msg: The original message. :type msg: `email.message.Message` :param error: Optional exception causing the bounce. The exception instance must have a `.message` attribute. :type error: Exception """ # Bounce a message back to the sender, with an error message if provided # in the exception argument. .sender might be None or the empty string. if not msg.sender: # We can't bounce the message if we don't know who it's supposed to go # to. return subject = msg.get('subject', _('(no subject)')) subject = oneline(subject, mlist.preferred_language.charset) if error is None: notice = _('[No bounce details are available]') else: notice = _(error.message) # Currently we always craft bounces as MIME messages. bmsg = UserNotification(msg.sender, mlist.owner_address, subject, lang=mlist.preferred_language) # BAW: Be sure you set the type before trying to attach, or you'll get # a MultipartConversionError. bmsg.set_type('multipart/mixed') txt = MIMEText(notice, _charset=mlist.preferred_language.charset) bmsg.attach(txt) bmsg.attach(MIMEMessage(msg)) bmsg.send(mlist)
def add_options(self): """See `Options`.""" self.parser.add_option( "-a", "--all", default=False, action="store_true", help=_( """\ Run the message through all the registered bounce modules. Normally this script stops at the first match.""" ), ) self.parser.add_option( "-m", "--module", type="string", help=_( """ Run the message through just the named bounce module.""" ), ) self.parser.add_option( "-l", "--list", default=False, action="store_true", help=_("List all available bounce modules and exit.") ) self.parser.add_option("-v", "--verbose", default=False, action="store_true", help=_("Increase verbosity."))
def add(self, parser, command_parser): """See `ICLISubCommand`.""" command_parser.add_argument( '-l', '--list', default=[], dest='lists', metavar='list', action='append', help=_("""Operate on this mailing list. Multiple --list options can be given. The argument can either be a List-ID or a fully qualified list name. Without this option, operate on the digests for all mailing lists.""")) command_parser.add_argument( '-s', '--send', default=False, action='store_true', help=_("""Send any collected digests right now, even if the size threshold has not yet been met.""")) command_parser.add_argument( '-b', '--bump', default=False, action='store_true', help=_("""Increment the digest volume number and reset the digest number to one. If given with --send, the volume number is incremented before any current digests are sent.""")) command_parser.add_argument( '-n', '--dry-run', default=False, action='store_true', help=_("""Don't actually do anything, but in conjunction with --verbose, show what would happen.""")) command_parser.add_argument( '-v', '--verbose', default=False, action='store_true', help=_("""Print some additional status."""))
def add(self, parser, command_parser): """See `ICLISubCommand`.""" self.parser = parser command_parser.add_argument( "-n", "--noprint", dest="doprint", default=True, action="store_false", help=_( """\ Don't attempt to pretty print the object. This is useful if there is some problem with the object and you just want to get an unpickled representation. Useful with 'bin/dumpdb -i <file>'. In that case, the list of unpickled objects will be left in a variable called 'm'.""" ), ) command_parser.add_argument( "-i", "--interactive", default=False, action="store_true", help=_( """\ Start an interactive Python session, with a variable called 'm' containing the list of unpickled objects.""" ), ) command_parser.add_argument("qfile", metavar="FILENAME", nargs=1, help=_("The queue file to dump."))
def add_members(self, mlist, args): """Add the members in a file to a mailing list. :param mlist: The mailing list to operate on. :type mlist: `IMailingList` :param args: The command line arguments. :type args: `argparse.Namespace` """ with ExitStack() as resources: if args.input_filename == '-': fp = sys.stdin else: fp = resources.enter_context( open(args.input_filename, 'r', encoding='utf-8')) for line in fp: # Ignore blank lines and lines that start with a '#'. if line.startswith('#') or len(line.strip()) == 0: continue # Parse the line and ensure that the values are unicodes. display_name, email = parseaddr(line) try: add_member(mlist, RequestRecord(email, display_name, DeliveryMode.regular, mlist.preferred_language.code)) except AlreadySubscribedError: # It's okay if the address is already subscribed, just # print a warning and continue. if not display_name: print(_('Already subscribed (skipping): $email')) else: print(_('Already subscribed (skipping): ' '$display_name <$email>'))
def send_welcome_message(mlist, member, language, text=''): """Send a welcome message to a subscriber. Prepending to the standard welcome message template is the mailing list's welcome message, if there is one. :param mlist: The mailing list. :type mlist: IMailingList :param member: The member to send the welcome message to. :param address: IMember :param language: The language of the response. :type language: ILanguage """ welcome_message = _get_message(mlist.welcome_message_uri, mlist, language) options_url = member.options_url # Get the text from the template. display_name = ('' if member.user is None else member.user.display_name) text = expand(welcome_message, dict( fqdn_listname=mlist.fqdn_listname, list_name=mlist.display_name, listinfo_uri=mlist.script_url('listinfo'), list_requests=mlist.request_address, user_name=display_name, user_address=member.address.email, user_options_uri=options_url, )) digmode = ('' if member.delivery_mode is DeliveryMode.regular else _(' (Digest mode)')) msg = UserNotification( formataddr((display_name, member.address.email)), mlist.request_address, _('Welcome to the "$mlist.display_name" mailing list${digmode}'), text, language) msg['X-No-Archive'] = 'yes' msg.send(mlist, verp=as_boolean(config.mta.verp_personalized_deliveries))
def process(self, args): """See `ICLISubCommand`.""" # Could be None or sequence of length 0. if args.listname is None: self.parser.error(_('List name is required')) return assert len(args.listname) == 1, ( 'Unexpected positional arguments: %s' % args.listname) fqdn_listname = args.listname[0] mlist = getUtility(IListManager).get(fqdn_listname) if mlist is None: self.parser.error(_('No such list: $fqdn_listname')) return if args.pickle_file is None: self.parser.error(_('config.pck file is required')) return assert len(args.pickle_file) == 1, ( 'Unexpected positional arguments: %s' % args.pickle_file) filename = args.pickle_file[0] with open(filename) as fp: while True: try: config_dict = cPickle.load(fp) except EOFError: break except cPickle.UnpicklingError: self.parser.error( _('Not a Mailman 2.1 configuration file: $filename')) return else: if not isinstance(config_dict, dict): print(_('Ignoring non-dictionary: {0!r}').format( config_dict), file=sys.stderr) continue import_config_pck(mlist, config_dict)
def add(self, parser, command_parser): """See `ICLISubCommand`.""" command_parser.add_argument( "-a", "--advertised", default=False, action="store_true", help=_("List only those mailing lists that are publicly advertised"), ) command_parser.add_argument( "-n", "--names", default=False, action="store_true", help=_("Show also the list names") ) command_parser.add_argument( "-d", "--descriptions", default=False, action="store_true", help=_("Show also the list descriptions") ) command_parser.add_argument("-q", "--quiet", default=False, action="store_true", help=_("Less verbosity")) command_parser.add_argument( "--domain", action="append", help=_( """\ List only those mailing lists hosted on the given domain, which must be the email host name. Multiple -d options may be given. """ ), )
def main(): """bin/mailman""" # Create the basic parser and add all globally common options. parser = argparse.ArgumentParser( description=_("""\ The GNU Mailman mailing list management system Copyright 1998-2012 by the Free Software Foundation, Inc. http://www.list.org """), formatter_class=argparse.RawDescriptionHelpFormatter) parser.add_argument( '-v', '--version', action='version', version=MAILMAN_VERSION_FULL, help=_('Print this version string and exit')) parser.add_argument( '-C', '--config', help=_("""\ Configuration file to use. If not given, the environment variable MAILMAN_CONFIG_FILE is consulted and used if set. If neither are given, a default configuration file is loaded.""")) # Look at all modules in the mailman.bin package and if they are prepared # to add a subcommand, let them do so. I'm still undecided as to whether # this should be pluggable or not. If so, then we'll probably have to # partially parse the arguments now, then initialize the system, then find # the plugins. Punt on this for now. subparser = parser.add_subparsers(title='Commands') subcommands = [] for command_class in find_components('mailman.commands', ICLISubCommand): command = command_class() verifyObject(ICLISubCommand, command) subcommands.append(command) # --help should display the subcommands by alphabetical order, except that # 'mailman help' should be first. def sort_function(command, other): """Sorting helper.""" if command.name == 'help': return -1 elif other.name == 'help': return 1 else: return cmp(command.name, other.name) subcommands.sort(cmp=sort_function) for command in subcommands: command_parser = subparser.add_parser( command.name, help=_(command.__doc__)) command.add(parser, command_parser) command_parser.set_defaults(func=command.process) args = parser.parse_args() if len(args.__dict__) == 0: # No arguments or subcommands were given. parser.print_help() parser.exit() # Initialize the system. Honor the -C flag if given. config_path = (None if args.config is None else os.path.abspath(os.path.expanduser(args.config))) initialize(config_path) # Perform the subcommand option. args.func(args)
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 main(): opts, args, parser = parseargs() initialize(opts.config) for name in config.list_manager.names: # The list must be locked in order to open the requests database mlist = MailList.MailList(name) try: count = IListRequests(mlist).count # While we're at it, let's evict yesterday's autoresponse data midnight_today = midnight() evictions = [] for sender in mlist.hold_and_cmd_autoresponses.keys(): date, respcount = mlist.hold_and_cmd_autoresponses[sender] if midnight(date) < midnight_today: evictions.append(sender) if evictions: for sender in evictions: del mlist.hold_and_cmd_autoresponses[sender] # This is the only place we've changed the list's database mlist.Save() if count: # Set the default language the the list's preferred language. _.default = mlist.preferred_language realname = mlist.real_name discarded = auto_discard(mlist) if discarded: count = count - discarded text = _('Notice: $discarded old request(s) ' 'automatically expired.\n\n') else: text = '' if count: text += Utils.maketext( 'checkdbs.txt', {'count' : count, 'mail_host': mlist.mail_host, 'adminDB' : mlist.GetScriptURL('admindb', absolute=1), 'real_name': realname, }, mlist=mlist) text += '\n' + pending_requests(mlist) subject = _('$count $realname moderator ' 'request(s) waiting') else: subject = _('$realname moderator request check result') msg = UserNotification(mlist.GetOwnerEmail(), mlist.GetBouncesEmail(), subject, text, mlist.preferred_language) msg.send(mlist, **{'tomoderators': True}) finally: mlist.Unlock()
def add(self, parser, command_parser): """See `ICLISubCommand`.""" self.parser = parser # Required positional arguments. command_parser.add_argument( 'listname', metavar='LISTNAME', nargs=1, help=_("""\ The 'fully qualified list name', i.e. the posting address of the mailing list to inject the message into.""")) command_parser.add_argument( 'pickle_file', metavar='FILENAME', nargs=1, help=_('The path to the config.pck file to import.'))
def add(self, parser, command_parser): """See `ICLISubCommand`.""" command_parser.add_argument( '-o', '--output', action='store', help=_("""\ File to send the output to. If not given, standard output is used.""")) command_parser.add_argument( '-v', '--verbose', action='store_true', help=_("""\ A more verbose output including the file system paths that Mailman is using."""))
def parseargs(): parser = optparse.OptionParser(version=MAILMAN_VERSION, usage=_("""\ %prog [options] [listname ...] Increment the digest volume number and reset the digest number to one. All the lists named on the command line are bumped. If no list names are given, all lists are bumped.""")) parser.add_option('-C', '--config', help=_('Alternative configuration file to use')) opts, args = parser.parse_args() return opts, args, parser
def add(self, parser, command_parser): """See `ICLISubCommand`.""" command_parser.add_argument( '-q', '--quiet', default=False, action='store_true', help=_('Suppress status messages')) # Required positional argument. command_parser.add_argument( 'listname', metavar='LISTNAME', nargs=1, help=_("""\ The 'fully qualified list name', i.e. the posting address of the mailing list."""))
class Acknowledge: """Send an acknowledgment.""" name = 'acknowledge' description = _("""Send an acknowledgment of a posting.""") def process(self, mlist, msg, msgdata): """See `IHandler`.""" # Extract the sender's address and find them in the user database sender = msgdata.get('original_sender', msg.sender) member = mlist.members.get_member(sender) if member is None or not member.acknowledge_posts: # Either the sender is not a member, in which case we can't know # whether they want an acknowlegment or not, or they are a member # who definitely does not want an acknowlegment. return # Okay, they are a member that wants an acknowledgment of their post. # Give them their original subject. BAW: do we want to use the # decoded header? original_subject = msgdata.get( 'origsubj', msg.get('subject', _('(no subject)'))) # Get the user's preferred language. language_manager = getUtility(ILanguageManager) language = (language_manager[msgdata['lang']] if 'lang' in msgdata else member.preferred_language) # Now get the acknowledgement template. display_name = mlist.display_name # noqa: F841 template = getUtility(ITemplateLoader).get( 'list:user:notice:post', mlist, language=language.code) text = expand(template, mlist, dict( subject=oneline(original_subject, in_unicode=True), # For backward compatibility. list_name=mlist.list_name, )) # Craft the outgoing message, with all headers and attributes # necessary for general delivery. Then enqueue it to the outgoing # queue. subject = _('$display_name post acknowledgment') usermsg = UserNotification(sender, mlist.bounces_address, subject, text, language) usermsg.send(mlist)
class ToArchive: """Add the message to the archives.""" name = 'to-archive' description = _('Add the message to the archives.') def process(self, mlist, msg, msgdata): """See `IHandler`.""" # Short circuits. if (msgdata.get('isdigest') or mlist.archive_policy is ArchivePolicy.never): return # Common practice seems to favor "X-No-Archive: yes". No other value # for this header seems to make sense, so we'll just test for it's # presence. I'm keeping "X-Archive: no" for backwards compatibility. if 'x-no-archive' in msg or msg.get('x-archive', '').lower() == 'no': return # Send the message to the archiver queue. config.switchboards['archive'].enqueue(msg, msgdata)
def send_goodbye_message(mlist, address, language): """Send a goodbye message to a subscriber. Prepending to the standard goodbye message template is the mailing list's goodbye message, if there is one. :param mlist: the mailing list :type mlist: IMailingList :param address: The address to respond to :type address: string :param language: the language of the response :type language: string """ goodbye_message = _get_message(mlist.goodbye_message_uri, mlist, language) msg = UserNotification( address, mlist.bounces_address, _('You have been unsubscribed from the $mlist.display_name ' 'mailing list'), goodbye_message, language) msg.send(mlist, verp=as_boolean(config.mta.verp_personalized_deliveries))
def hold_message(mlist, msg, msgdata=None, reason=None): """Hold a message for moderator approval. The message is added to the mailing list's request database. :param mlist: The mailing list to hold the message on. :param msg: The message to hold. :param msgdata: Optional message metadata to hold. If not given, a new metadata dictionary is created and held with the message. :param reason: Optional string reason why the message is being held. If not given, the empty string is used. :return: An id used to handle the held message later. """ if msgdata is None: msgdata = {} else: # Make a copy of msgdata so that subsequent changes won't corrupt the # request database. TBD: remove the `filebase' key since this will # not be relevant when the message is resurrected. msgdata = msgdata.copy() if reason is None: reason = '' # Add the message to the message store. It is required to have a # Message-ID header. message_id = msg.get('message-id') if message_id is None: msg['Message-ID'] = message_id = make_msgid() elif isinstance(message_id, bytes): message_id = message_id.decode('ascii') getUtility(IMessageStore).add(msg) # Prepare the message metadata with some extra information needed only by # the moderation interface. msgdata['_mod_message_id'] = message_id msgdata['_mod_listid'] = mlist.list_id msgdata['_mod_sender'] = msg.sender msgdata['_mod_subject'] = msg.get('subject', _('(no subject)')) msgdata['_mod_reason'] = reason msgdata['_mod_hold_date'] = now().isoformat() # Now hold this request. We'll use the message_id as the key. requestsdb = IListRequests(mlist) request_id = requestsdb.hold_request(RequestType.held_message, message_id, msgdata) return request_id
def digests(ctx, list_ids, send, bump, dry_run, verbose, periodic): # send and periodic options are mutually exclusive, if they both are # specified, exit. if send and periodic: print(_('--send and --periodic flags cannot be used together'), file=sys.stderr) exit(1) list_manager = getUtility(IListManager) if list_ids: lists = [] for spec in list_ids: # We'll accept list-ids or fqdn list names. if '@' in spec: mlist = list_manager.get(spec) else: mlist = list_manager.get_by_list_id(spec) if mlist is None: print(_('No such list found: $spec'), file=sys.stderr) else: lists.append(mlist) else: lists = list(list_manager.mailing_lists) if bump: for mlist in lists: if verbose: print(_('\ $mlist.list_id is at volume $mlist.volume, number \ ${mlist.next_digest_number}')) if not dry_run: bump_digest_number_and_volume(mlist) if verbose: print(_('\ $mlist.list_id bumped to volume $mlist.volume, number \ ${mlist.next_digest_number}')) if send: for mlist in lists: if verbose: print(_('\ $mlist.list_id sent volume $mlist.volume, number ${mlist.next_digest_number}')) if not dry_run: maybe_send_digest_now(mlist, force=True) if periodic: for mlist in lists: if mlist.digest_send_periodic: if verbose: print(_('\ $mlist.list_id sent volume $mlist.volume, number ${mlist.next_digest_number}')) if not dry_run: maybe_send_digest_now(mlist, force=True)
def all_same_charset(mlist, msgdata, subject, prefix, prefix_pattern, ws): list_charset = mlist.preferred_language.charset chunks = [] for chunk, charset in decode_header(subject.encode()): if charset is None: charset = 'us-ascii' if isinstance(chunk, str): chunks.append(chunk) else: try: chunks.append(chunk.decode(charset)) except LookupError: # The charset value is unknown. return None if charset != list_charset: return None subject_text = EMPTYSTRING.join(chunks) # At this point, the subject may become null if someone posted mail # with "Subject: [subject prefix]". if subject_text.strip() == '': with _.push(mlist.preferred_language.code): subject_text = _('(no subject)') else: subject_text = re.sub(prefix_pattern, '', subject_text) msgdata['stripped_subject'] = subject_text rematch = re.match(RE_PATTERN, subject_text, re.I) if rematch: subject_text = subject_text[rematch.end():] recolon = 'Re: ' else: recolon = '' lines = subject_text.splitlines() # If the subject was only the prefix or Re:, the text could be null. first_line = [] if lines: first_line = [lines[0]] if recolon: first_line.insert(0, recolon) if prefix: first_line.insert(0, prefix) subject_text = EMPTYSTRING.join(first_line) return Header(subject_text, charset=list_charset, continuation_ws=ws)
class BuiltInChain: """Default built-in chain.""" name = 'default-posting-chain' description = _('The built-in moderation chain.') _link_descriptions = ( ('approved', LinkAction.jump, 'accept'), ('emergency', LinkAction.jump, 'hold'), ('loop', LinkAction.jump, 'discard'), # Discard emails from banned addresses. ('banned-address', LinkAction.jump, 'discard'), # Determine whether the member or nonmember has an action shortcut. ('member-moderation', LinkAction.jump, 'moderation'), # Take a detour through the header matching chain. ('truth', LinkAction.detour, 'header-match'), # Check for nonmember moderation. ('nonmember-moderation', LinkAction.jump, 'moderation'), # Do all of the following before deciding whether to hold the message. ('administrivia', LinkAction.defer, None), ('implicit-dest', LinkAction.defer, None), ('max-recipients', LinkAction.defer, None), ('max-size', LinkAction.defer, None), ('news-moderation', LinkAction.defer, None), ('no-subject', LinkAction.defer, None), ('suspicious-header', LinkAction.defer, None), # Now if any of the above hit, jump to the hold chain. ('any', LinkAction.jump, 'hold'), # Finally, the builtin chain jumps to acceptance. ('truth', LinkAction.jump, 'accept'), ) def __init__(self): self._cached_links = None def get_links(self, mlist, msg, msgdata): """See `IChain`.""" if self._cached_links is None: self._cached_links = links = [] for rule, action, chain in self._link_descriptions: links.append(Link(rule, action, chain)) return iter(self._cached_links)
class NoSubject: """The no-Subject rule.""" name = 'no-subject' description = _('Catch messages with no, or empty, Subject headers.') record = True def check(self, mlist, msg, msgdata): """See `IRule`.""" # Convert the header value to a str because it may be an # email.header.Header instance. subject = str(msg.get('subject', '')).strip() if 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 no subject')) return True return False
def add(self, parser, command_parser): """See `ICLISubCommand`.""" self.parser = parser command_parser.add_argument( '-a', '--add', dest='input_filename', metavar='FILENAME', help=_("""\ Add all member addresses in FILENAME. FILENAME can be '-' to indicate standard input. Blank lines and lines That start with a '#' are ignored. Without this option, this command displays mailing list members.""")) command_parser.add_argument( '-o', '--output', dest='output_filename', metavar='FILENAME', help=_("""Display output to FILENAME instead of stdout. FILENAME can be '-' to indicate standard output.""")) command_parser.add_argument( '-r', '--regular', default=None, action='store_true', help=_('Display only regular delivery members.')) command_parser.add_argument( '-d', '--digest', default=None, metavar='KIND', # BAW 2010-01-23 summary digests are not really supported yet. choices=('any', 'plaintext', 'mime'), help=_("""Display only digest members of KIND. 'any' means any digest type, 'plaintext' means only plain text (RFC 1153) type digests, 'mime' means MIME type digests.""")) command_parser.add_argument( '-n', '--nomail', default=None, metavar='WHY', choices=('enabled', 'any', 'unknown' 'byadmin', 'byuser', 'bybounces'), help=_("""Display only members with a given delivery status. 'enabled' means all members whose delivery is enabled, 'any' means members whose delivery is disabled for any reason, 'byuser' means that the member disabled their own delivery, 'bybounces' means that delivery was disabled by the automated bounce processor, 'byadmin' means delivery was disabled by the list administrator or moderator, and 'unknown' means that delivery was disabled for unknown (legacy) reasons.""")) # Required positional argument. command_parser.add_argument( 'listname', metavar='LISTNAME', nargs=1, help=_("""\ The 'fully qualified list name', i.e. the posting address of the mailing list. It must be a valid email address and the domain must be registered with Mailman. List names are forced to lower case."""))
class BannedAddress: """The banned address rule.""" name = 'banned-address' description = _('Match messages sent by banned addresses.') record = True def check(self, mlist, msg, msgdata): """See `IRule`.""" ban_manager = IBanManager(mlist) for sender in msg.senders: if ban_manager.is_banned(sender): msgdata['moderation_sender'] = sender with _.defer_translation(): # This will be translated at the point of use. msgdata.setdefault('moderation_reasons', []).append( (_('Message sender {} is banned from this list'), sender)) return True return False
class ModeratedNewsgroup: """The news moderation rule.""" name = 'news-moderation' description = _( """Match all messages posted to a mailing list that gateways to a moderated newsgroup. """) record = True def check(self, mlist, msg, msgdata): """See `IRule`.""" if mlist.newsgroup_moderation is NewsgroupModeration.moderated: msgdata['moderation_sender'] = msg.sender with _.defer_translation(): # This will be translated at the point of use. msgdata.setdefault('moderation_reasons', []).append( _('Post to a moderated newsgroup gateway')) return True return False
class AcceptChain(TerminalChainBase): """Accept the message for posting.""" name = 'accept' description = _('Accept a message.') 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) config.switchboards['pipeline'].enqueue(msg, msgdata) log.info('ACCEPT: %s', msg.get('message-id', 'n/a')) notify(AcceptEvent(mlist, msg, msgdata, self))
class Loop: """Look for a posting loop.""" name = 'loop' description = _('Look for a posting loop.') record = True def check(self, mlist, msg, msgdata): """See `IRule`.""" # Has this message already been posted to this list? list_posts = set(value.strip().lower() for value in msg.get_all('list-post', [])) if mlist.posting_address in list_posts: msgdata['moderation_sender'] = msg.sender with _.defer_translation(): # This will be translated at the point of use. msgdata.setdefault('moderation_reasons', []).append( _('Message has already been posted to this list')) return True return False
def send_admin_subscription_notice(mlist, address, display_name): """Send the list administrators a subscription notice. :param mlist: The mailing list. :type mlist: IMailingList :param address: The address being subscribed. :type address: string :param display_name: The name of the subscriber. :type display_name: string """ with _.using(mlist.preferred_language.code): subject = _('$mlist.display_name subscription notification') text = make( 'adminsubscribeack.txt', mailing_list=mlist, listname=mlist.display_name, member=formataddr((display_name, address)), ) msg = OwnerNotification(mlist, subject, text, roster=mlist.administrators) msg.send(mlist)
def send_admin_removal_notice(mlist, address, display_name): """Send the list administrators a membership removed due to bounce notice. :param mlist: The mailing list. :type mlist: IMailingList :param address: The address of the member :type address: string :param display_name: The name of the subscriber :type display_name: string """ member = formataddr((display_name, address)) data = {'member': member, 'mlist': mlist.display_name} with _.using(mlist.preferred_language.code): subject = _('$member unsubscribed from ${mlist.display_name} ' 'mailing list due to bounces') text = expand( getUtility(ITemplateLoader).get('list:admin:notice:removal', mlist), mlist, data) msg = OwnerNotification(mlist, subject, text, roster=mlist.administrators) msg.send(mlist)
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 = expand( getUtility(ITemplateLoader).get('list:admin:notice:unsubscribe', mlist), mlist, dict(member=formataddr((display_name, email)), )) msg = OwnerNotification(mlist, subject, text, roster=mlist.administrators) msg.send(mlist)
class NonmemberModeration: """The nonmember moderation rule.""" name = 'nonmember-moderation' description = _('Match messages sent by nonmembers.') record = True def check(self, mlist, msg, msgdata): """See `IRule`.""" user_manager = getUtility(IUserManager) # First ensure that all senders are already either members or # nonmembers. If they are not subscribed in some role to the mailing # list, make them nonmembers. for sender in msg.senders: if (mlist.members.get_member(sender) is None and mlist.nonmembers.get_member(sender) is None): # The address is neither a member nor nonmember. address = user_manager.get_address(sender) assert address is not None, ( 'Posting address is not registered: {0}'.format(sender)) mlist.subscribe(address, MemberRole.nonmember) ## # If a member is found, the member-moderation rule takes precedence. for sender in msg.senders: if mlist.members.get_member(sender) is not None: return False # Do nonmember moderation check. for sender in msg.senders: nonmember = mlist.nonmembers.get_member(sender) action = (None if nonmember is None else nonmember.moderation_action) if action is Action.defer: # The regular moderation rules apply. return False elif action is not None: # We must stringify the moderation action so that it can be # stored in the pending request table. msgdata['moderation_action'] = action.name msgdata['moderation_sender'] = sender return True # The sender must be a member, so this rule does not match. return False
class ToDigest: """Add the message to the digest, possibly sending it.""" name = 'to-digest' description = _('Add the message to the digest, possibly sending it.') def process(self, mlist, msg, msgdata): """See `IHandler`.""" # Short-circuit if digests are not enabled or if this message already # is a digest. if not mlist.digests_enabled or msgdata.get('isdigest'): return # Open the mailbox that will be used to collect the current digest. mailbox_path = os.path.join(mlist.data_path, 'digest.mmdf') # Create parent directory of 'digest.mmdf' if not present if not os.path.exists(mlist.data_path): os.mkdir(mlist.data_path) # Lock the mailbox and append the message. with Mailbox(mailbox_path, create=True) as mbox: mbox.add(msg) maybe_send_digest_now(mlist)
class RejectChain(TerminalChainBase): """Reject/bounce a message.""" name = 'reject' description = _('Reject/bounce a message and stop processing.') 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) # XXX Exception/reason bounce_message(mlist, msg) log.info('REJECT: %s', msg.get('message-id', 'n/a')) notify(RejectEvent(mlist, msg, msgdata, self))
def has_matching_bounce_header(mlist, msg, msgdata): """Does the message have a matching bounce header? :param mlist: The mailing list the message is destined for. :param msg: The email message object. :return: True if a header field matches a regexp in the bounce_matching_header mailing list variable. """ for header, cre, line in _parse_matching_header_opt(mlist): for value in msg.get_all(header, []): # Convert the header value to a str because it may be an # email.header.Header instance. if cre.search(str(value)): msgdata['moderation_sender'] = msg.sender with _.defer_translation(): # This will be translated at the point of use. msgdata.setdefault('moderation_reasons', []).append((_( 'Header "{}" matched a bounce_matching_header line'), str(value))) return True return False
class Help: """The email 'help' command.""" name = 'help' argument_description = '[command]' description = _('Get help about available email commands.') short_description = description 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
class Confirm: """The email 'confirm' command.""" name = 'confirm' argument_description = 'token' description = _('Confirm a subscription request.') short_description = description def process(self, mlist, msg, msgdata, arguments, results): """See `IEmailCommand`.""" # The token must be in the arguments. if len(arguments) == 0: print(_('No confirmation token found'), file=results) return ContinueProcessing.no # Make sure we don't try to confirm the same token more than once. token = arguments[0] tokens = getattr(results, 'confirms', set()) if token in tokens: # Do not try to confirm this one again. return ContinueProcessing.yes tokens.add(token) results.confirms = tokens try: token, token_owner, member = IRegistrar(mlist).confirm(token) if token is None: assert token_owner is TokenOwner.no_one, token_owner assert member is not None, member succeeded = True else: assert token_owner is not TokenOwner.no_one, token_owner assert member is None, member succeeded = False except LookupError: # The token must not exist in the database. succeeded = False if succeeded: print(_('Confirmed'), file=results) return ContinueProcessing.yes print(_('Confirmation token did not match'), file=results) return ContinueProcessing.no
def add(self, parser, command_parser): """See `ICLISubCommand`.""" self.parser = parser command_parser.add_argument('--language', metavar='CODE', help=_("""\ Set the list's preferred language to CODE, which must be a registered two letter language code.""")) command_parser.add_argument('-o', '--owner', action='append', default=[], dest='owners', metavar='OWNER', help=_("""\ Specify a listowner email address. If the address is not currently registered with Mailman, the address is registered and linked to a user. Mailman will send a confirmation message to the address, but it will also send a list creation notice to the address. More than one owner can be specified.""")) command_parser.add_argument('-n', '--notify', default=False, action='store_true', help=_("""\ Notify the list owner by email that their mailing list has been created.""")) command_parser.add_argument('-q', '--quiet', default=False, action='store_true', help=_('Print less output.')) command_parser.add_argument('-d', '--domain', default=False, action='store_true', help=_("""\ Register the mailing list's domain if not yet registered.""")) # Required positional argument. command_parser.add_argument('listname', metavar='LISTNAME', nargs=1, help=_("""\ The 'fully qualified list name', i.e. the posting address of the mailing list. It must be a valid email address and the domain must be registered with Mailman. List names are forced to lower case."""))
def parseargs(): parser = optparse.OptionParser(version=MAILMAN_VERSION, usage=_("""\ %prog [options] regex [regex ...] Find all lists that a member's address is on. The interaction between -l and -x (see below) is as follows. If any -l option is given then only the named list will be included in the search. If any -x option is given but no -l option is given, then all lists will be search except those specifically excluded. Regular expression syntax uses the Python 're' module. Complete specifications are at: http://www.python.org/doc/current/lib/module-re.html Address matches are case-insensitive, but case-preserved addresses are displayed.""")) parser.add_option('-l', '--listname', type='string', default=[], action='append', dest='listnames', help=_('Include only the named list in the search')) parser.add_option('-x', '--exclude', type='string', default=[], action='append', dest='excludes', help=_('Exclude the named list from the search')) parser.add_option('-w', '--owners', default=False, action='store_true', help=_('Search list owners as well as members')) parser.add_option('-C', '--config', help=_('Alternative configuration file to use')) opts, args = parser.parse_args() if not args: parser.print_help() print >> sys.stderr, _('Search regular expression required') sys.exit(1) return parser, opts, args
class Emergency: """The emergency hold rule.""" name = 'emergency' description = _( """The mailing list is in emergency hold and this message was not pre-approved by the list administrator. """) record = True def check(self, mlist, msg, msgdata): """See `IRule`.""" if mlist.emergency and not msgdata.get('moderator_approved'): msgdata['moderation_sender'] = msg.sender with _.defer_translation(): # This will be translated at the point of use. msgdata.setdefault('moderation_reasons', []).append( _('Emergency moderation is in effect for this list')) return True return False
def handle_ConfirmationNeededEvent(event): if not isinstance(event, ConfirmationNeededEvent): return # There are three ways for a user to confirm their subscription. They # can reply to the original message and let the VERP'd return address # encode the token, they can reply to the robot and keep the token in # the Subject header, or they can click on the URL in the body of the # message and confirm through the web. subject = 'confirm ' + event.token confirm_address = event.mlist.confirm_address(event.token) # For i18n interpolation. confirm_url = event.mlist.domain.confirm_url(event.token) # noqa email_address = event.email domain_name = event.mlist.domain.mail_host # noqa contact_address = event.mlist.owner_address # noqa # Send a verification email to the address. template = getUtility(ITemplateLoader).get( 'mailman:///{}/{}/confirm.txt'.format( event.mlist.fqdn_listname, event.mlist.preferred_language.code)) text = _(template) msg = UserNotification(email_address, confirm_address, subject, text) msg.send(event.mlist, add_precedence=False)
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 _build_detail(requestdb, subs, unsubs): """Builds the detail of held messages and pending subscriptions and unsubscriptions for the body of the notification email. """ detail = '' if len(subs) > 0: detail += _('\nHeld Subscriptions:\n') for sub in subs: detail += ' ' + _('User: {}\n').format(sub) if len(unsubs) > 0: detail += _('\nHeld Unsubscriptions:\n') for unsub in unsubs: detail += ' ' + _('User: {}\n').format(unsub) if requestdb.count_of(RequestType.held_message) > 0: detail += _('\nHeld Messages:\n') for rq in requestdb.of_type(RequestType.held_message): key, data = requestdb.get_request(rq.id) sender = data['_mod_sender'] subject = data['_mod_subject'] reason = data['_mod_reason'] detail += ' ' + _('Sender: {}\n').format(sender) detail += ' ' + _('Subject: {}\n').format(subject) detail += ' ' + _('Reason: {}\n\n').format(reason) return detail
class DMARCMitigation: """The DMARC mitigation rule.""" name = 'dmarc-mitigation' description = _('Find DMARC policy of From: domain.') record = True 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
class ToDigest: """Add the message to the digest, possibly sending it.""" name = 'to-digest' description = _('Add the message to the digest, possibly sending it.') def process(self, mlist, msg, msgdata): """See `IHandler`.""" # Short circuit for non-digestable messages. if not mlist.digestable or msgdata.get('isdigest'): return # Open the mailbox that will be used to collect the current digest. mailbox_path = os.path.join(mlist.data_path, 'digest.mmdf') # Lock the mailbox and append the message. with Mailbox(mailbox_path, create=True) as mbox: mbox.add(msg) # Calculate the current size of the mailbox file. This will not tell # us exactly how big the resulting MIME and rfc1153 digest will # actually be, but it's the most easily available metric to decide # whether the size threshold has been reached. size = os.path.getsize(mailbox_path) if size >= mlist.digest_size_threshold * 1024.0: # The digest is ready to send. Because we don't want to hold up # this process with crafting the digest, we're going to move the # digest file to a safe place, then craft a fake message for the # DigestRunner as a trigger for it to build and send the digest. mailbox_dest = os.path.join( mlist.data_path, 'digest.{0.volume}.{0.next_digest_number}.mmdf'.format(mlist)) volume = mlist.volume digest_number = mlist.next_digest_number bump_digest_number_and_volume(mlist) os.rename(mailbox_path, mailbox_dest) config.switchboards['digest'].enqueue(Message(), listid=mlist.list_id, digest_path=mailbox_dest, volume=volume, digest_number=digest_number)