Esempio n. 1
0
 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
Esempio n. 2
0
def _refuse(mlist, request, recip, comment, origmsg=None, lang=None):
    # As this message is going to the requester, try to set the language to
    # his/her language choice, if they are a member.  Otherwise use the list's
    # preferred language.
    display_name = mlist.display_name
    if lang is None:
        member = mlist.members.get_member(recip)
        lang = (mlist.preferred_language
                if member is None
                else member.preferred_language)
    text = make('refuse.txt',
                mailing_list=mlist,
                language=lang.code,
                listname=mlist.fqdn_listname,
                request=request,
                reason=comment,
                adminaddr=mlist.owner_address,
                )
    with _.using(lang.code):
        # add in original message, but not wrap/filled
        if origmsg:
            text = NL.join(
                [text,
                 '---------- ' + _('Original Message') + ' ----------',
                 str(origmsg)
                 ])
        subject = _('Request to mailing list "$display_name" rejected')
    msg = UserNotification(recip, mlist.bounces_address, subject, text, lang)
    msg.send(mlist)
Esempio n. 3
0
 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
Esempio n. 4
0
 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."""))
Esempio n. 5
0
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)
Esempio n. 6
0
 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.
         """))
Esempio n. 7
0
    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
Esempio n. 8
0
 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
Esempio n. 9
0
 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."""))
Esempio n. 10
0
    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.'))
Esempio n. 11
0
    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."""))
Esempio n. 12
0
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
Esempio n. 13
0
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)
Esempio n. 14
0
    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')
Esempio n. 15
0
 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
Esempio n. 16
0
def bounce_message(mlist, msg, error=None):
    """Bounce the message back to the original author.

    :param mlist: The mailing list that the message was posted to.
    :type mlist: `IMailingList`
    :param msg: The original message.
    :type msg: `email.message.Message`
    :param error: Optional exception causing the bounce.  The exception
        instance must have a `.message` attribute.
    :type error: Exception
    """
    # Bounce a message back to the sender, with an error message if provided
    # in the exception argument.  .sender might be None or the empty string.
    if not msg.sender:
        # We can't bounce the message if we don't know who it's supposed to go
        # to.
        return
    subject = msg.get('subject', _('(no subject)'))
    subject = oneline(subject, mlist.preferred_language.charset)
    if error is None:
        notice = _('[No bounce details are available]')
    else:
        notice = _(error.message)
    # Currently we always craft bounces as MIME messages.
    bmsg = UserNotification(msg.sender, mlist.owner_address, subject,
                            lang=mlist.preferred_language)
    # BAW: Be sure you set the type before trying to attach, or you'll get
    # a MultipartConversionError.
    bmsg.set_type('multipart/mixed')
    txt = MIMEText(notice, _charset=mlist.preferred_language.charset)
    bmsg.attach(txt)
    bmsg.attach(MIMEMessage(msg))
    bmsg.send(mlist)
Esempio n. 17
0
    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."))
Esempio n. 18
0
    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."""))
Esempio n. 19
0
 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."))
Esempio n. 20
0
    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>'))
Esempio n. 21
0
def send_welcome_message(mlist, member, language, text=''):
    """Send a welcome message to a subscriber.

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

    :param mlist: The mailing list.
    :type mlist: IMailingList
    :param member: The member to send the welcome message to.
    :param address: IMember
    :param language: The language of the response.
    :type language: ILanguage
    """
    welcome_message = _get_message(mlist.welcome_message_uri, mlist, language)
    options_url = member.options_url
    # Get the text from the template.
    display_name = ('' if member.user is None else member.user.display_name)
    text = expand(welcome_message, dict(
        fqdn_listname=mlist.fqdn_listname,
        list_name=mlist.display_name,
        listinfo_uri=mlist.script_url('listinfo'),
        list_requests=mlist.request_address,
        user_name=display_name,
        user_address=member.address.email,
        user_options_uri=options_url,
        ))
    digmode = ('' if member.delivery_mode is DeliveryMode.regular
               else _(' (Digest mode)'))
    msg = UserNotification(
        formataddr((display_name, member.address.email)),
        mlist.request_address,
        _('Welcome to the "$mlist.display_name" mailing list${digmode}'),
        text, language)
    msg['X-No-Archive'] = 'yes'
    msg.send(mlist, verp=as_boolean(config.mta.verp_personalized_deliveries))
Esempio n. 22
0
 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)
Esempio n. 23
0
 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.
         """
         ),
     )
Esempio n. 24
0
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)
Esempio n. 25
0
def send_welcome_message(mlist, address, language, delivery_mode, text=''):
    """Send a welcome message to a subscriber.

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

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

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

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

    :param mlist: the mailing list
    :type mlist: IMailingList
    :param address: The address to respond to
    :type address: string
    :param language: the language of the response
    :type language: string
    """
    goodbye_message = _get_message(mlist.goodbye_message_uri, mlist, language)
    msg = UserNotification(
        address, mlist.bounces_address,
        _('You have been unsubscribed from the $mlist.display_name '
          'mailing list'), goodbye_message, language)
    msg.send(mlist, verp=as_boolean(config.mta.verp_personalized_deliveries))
Esempio n. 34
0
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
Esempio n. 35
0
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)
Esempio n. 36
0
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)
Esempio n. 37
0
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)
Esempio n. 38
0
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
Esempio n. 39
0
 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."""))
Esempio n. 40
0
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
Esempio n. 41
0
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
Esempio n. 42
0
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))
Esempio n. 43
0
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
Esempio n. 44
0
def send_admin_subscription_notice(mlist, address, display_name):
    """Send the list administrators a subscription notice.

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

    :param mlist: The mailing list.
    :type mlist: IMailingList
    :param address: The address of the member
    :type address: string
    :param display_name: The name of the subscriber
    :type display_name: string
    """
    member = formataddr((display_name, address))
    data = {'member': member, 'mlist': mlist.display_name}
    with _.using(mlist.preferred_language.code):
        subject = _('$member unsubscribed from ${mlist.display_name} '
                    'mailing list due to bounces')
    text = expand(
        getUtility(ITemplateLoader).get('list:admin:notice:removal', mlist),
        mlist, data)
    msg = OwnerNotification(mlist, subject, text, roster=mlist.administrators)
    msg.send(mlist)
Esempio n. 46
0
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)
Esempio n. 47
0
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
Esempio n. 48
0
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)
Esempio n. 49
0
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))
Esempio n. 50
0
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
Esempio n. 51
0
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
Esempio n. 52
0
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
Esempio n. 53
0
 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."""))
Esempio n. 54
0
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
Esempio n. 55
0
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
Esempio n. 56
0
def handle_ConfirmationNeededEvent(event):
    if not isinstance(event, ConfirmationNeededEvent):
        return
    # There are three ways for a user to confirm their subscription.  They
    # can reply to the original message and let the VERP'd return address
    # encode the token, they can reply to the robot and keep the token in
    # the Subject header, or they can click on the URL in the body of the
    # message and confirm through the web.
    subject = 'confirm ' + event.token
    confirm_address = event.mlist.confirm_address(event.token)
    # For i18n interpolation.
    confirm_url = event.mlist.domain.confirm_url(event.token)  # noqa
    email_address = event.email
    domain_name = event.mlist.domain.mail_host  # noqa
    contact_address = event.mlist.owner_address  # noqa
    # Send a verification email to the address.
    template = getUtility(ITemplateLoader).get(
        'mailman:///{}/{}/confirm.txt'.format(
            event.mlist.fqdn_listname, event.mlist.preferred_language.code))
    text = _(template)
    msg = UserNotification(email_address, confirm_address, subject, text)
    msg.send(event.mlist, add_precedence=False)
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
Esempio n. 59
0
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
Esempio n. 60
0
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)