diff options
Diffstat (limited to 'Mailman/docs/hold.txt')
| -rw-r--r-- | Mailman/docs/hold.txt | 373 |
1 files changed, 373 insertions, 0 deletions
diff --git a/Mailman/docs/hold.txt b/Mailman/docs/hold.txt new file mode 100644 index 000000000..7a06c4b81 --- /dev/null +++ b/Mailman/docs/hold.txt @@ -0,0 +1,373 @@ +Holding messages +================ + +One of the most important functions of Mailman is to moderate messages by +holding some for approval before they will post to the mailing list. Messages +are held when they meet any of a number of criteria. + + >>> import os + >>> import errno + >>> from Mailman.Handlers.Hold import process + >>> from Mailman.Queue.Switchboard import Switchboard + >>> from Mailman.configuration import config + >>> from Mailman.database import flush + >>> mlist = config.list_manager.create('_xtest@example.com') + >>> mlist.preferred_language = 'en' + >>> mlist.real_name = '_XTest' + >>> # XXX This will almost certainly change once we've worked out the web + >>> # space layout for mailing lists now. + >>> mlist._data.web_page_url = 'http://lists.example.com/' + >>> flush() + +XXX The Hold handler requires that the mailing list be locked because it +touches the pending database. Eventually the pending database should be moved +into the real database so that the lock is no longer necessary. + + >>> mlist.Lock() + +Here's a helper function used when we don't care about what's in the virgin +queue or in the pending database. + + >>> switchboard = Switchboard(config.VIRGINQUEUE_DIR) + >>> def clear(): + ... for filebase in switchboard.files: + ... msg, msgdata = switchboard.dequeue(filebase) + ... switchboard.finish(filebase) + ... for holdfile in os.listdir(config.DATA_DIR): + ... if holdfile.startswith('heldmsg-'): + ... os.unlink(os.path.join(config.DATA_DIR, holdfile)) + ... try: + ... os.unlink(os.path.join(config.DATA_DIR, 'pending.db')) + ... except OSError, e: + ... if e.errno <> errno.ENOENT: + ... raise + + +Short circuiting +---------------- + +If the message metadata indicates that the message is pre-approved, then the +handler returns immediately. + + >>> from email import message_from_string + >>> from Mailman.Message import Message + >>> msg = message_from_string("""\ + ... From: aperson@example.com + ... + ... An important message. + ... """, Message) + >>> msgdata = {'approved': True} + >>> process(mlist, msg, msgdata) + >>> print msg.as_string() + From: aperson@example.com + <BLANKLINE> + An important message. + <BLANKLINE> + >>> msgdata + {'approved': True} + + +Administrivia +------------- + +Mailman scans parts of the message for administrivia, meaning text that looks +like an email command. This is to prevent people sending 'help' or +'subscribe' message, etc. to the list members. First, we enable the scanning +of administrivia for the list. + + >>> mlist.administrivia = True + >>> flush() + >>> msg = message_from_string("""\ + ... From: aperson@example.com + ... Subject: unsubscribe + ... + ... """, Message) + >>> process(mlist, msg, {}) + Traceback (most recent call last): + ... + Administrivia + >>> clear() + + +Maximum number of recipients +---------------------------- + +Mailman will hold messages that have more than a specified number of explicit +recipients. + + >>> mlist.max_num_recipients = 5 + >>> flush() + >>> msg = message_from_string("""\ + ... From: aperson@example.com + ... To: _xtest@example.com, bperson@example.com + ... Cc: cperson@example.com + ... Cc: dperson@example.com (Dan Person) + ... To: Elly Q. Person <eperson@example.com> + ... + ... Hey folks! + ... """, Message) + >>> process(mlist, msg, {}) + Traceback (most recent call last): + ... + TooManyRecipients + >>> clear() + + +Implicit destination +-------------------- + +Mailman will hold messages that have implicit destination, meaning that the +mailing list's posting address isn't included in the explicit recipients. + + >>> mlist.require_explicit_destination = True + >>> mlist.acceptable_aliases = '' + >>> flush() + >>> msg = message_from_string("""\ + ... From: aperson@example.org + ... Subject: An implicit message + ... + ... """, Message) + >>> process(mlist, msg, {}) + Traceback (most recent call last): + ... + ImplicitDestination + >>> clear() + +A message gated from NNTP will obviously have an implicit destination. Such +gated messages will not be held for implicit destination because it's assumed +that Mailman pulled it from the appropriate news group. + + >>> msgdata = {'fromusenet': True} + >>> process(mlist, msg, msgdata) + >>> print msg.as_string() + From: aperson@example.org + Subject: An implicit message + <BLANKLINE> + >>> print msgdata + {'fromusenet': True} + + +Suspicious headers +------------------ + +Suspicious headers are a way for Mailman to hold messages that match a +particular regular expression. This mostly historical feature is fairly +confusing to users, and the list attribute that controls this is misnamed. + + >>> mlist.bounce_matching_headers = 'From: .*person@(blah.)?example.com' + >>> flush() + >>> msg = message_from_string("""\ + ... From: aperson@example.com + ... To: _xtest@example.com + ... Subject: An implicit message + ... + ... """, Message) + >>> process(mlist, msg, {}) + Traceback (most recent call last): + ... + SuspiciousHeaders + >>> clear() + +But if the header doesn't match the regular expression, it'll get posted just +fine. This one comes from a .org address. + + >>> msg = message_from_string("""\ + ... From: aperson@example.org + ... To: _xtest@example.com + ... Subject: An implicit message + ... + ... """, Message) + >>> msgdata = {} + >>> process(mlist, msg, msgdata) + >>> print msgdata + {} + +Just a bit of clean up. + + >>> mlist.bounce_matching_headers = None + >>> flush() + + +Message size +------------ + +Mailman can hold messages that are bigger than a given size. Generally this +is used to prevent huge attachments from getting posted to the list. This +value is calculated in terms of KB (1024 bytes). + + >>> mlist.max_message_size = 1 + >>> flush() + >>> one_line = 'x' * 79 + >>> big_body = '\n'.join([one_line] * 15) + >>> msg = message_from_string("""\ + ... From: aperson@example.com + ... To: _xtest@example.com + ... + ... """ + big_body, Message) + >>> process(mlist, msg, {}) + Traceback (most recent call last): + ... + MessageTooBig + >>> clear() + + +Hold Notifications +------------------ + +Whenever Mailman holds a message, it sends notifications both to the list +owner and to the original sender, as long as it is configured to do so. We +can show this by first holding a message. + + >>> mlist.respond_to_post_requests = True + >>> mlist.admin_immed_notify = True + >>> flush() + >>> msg = message_from_string("""\ + ... From: aperson@example.com + ... + ... """, Message) + >>> process(mlist, msg, {}) + Traceback (most recent call last): + ... + ImplicitDestination + +There should be two messages in the virgin queue, one to the list owner and +one to the original author. + + >>> len(switchboard.files) + 2 + >>> qfiles = {} + >>> for filebase in switchboard.files: + ... qmsg, qdata = switchboard.dequeue(filebase) + ... switchboard.finish(filebase) + ... qfiles[qmsg['to']] = qmsg, qdata + >>> qmsg, qdata = qfiles['_xtest-owner@example.com'] + >>> print qmsg.as_string() + Subject: _xtest post from aperson@example.com requires approval + From: _xtest-owner@example.com + To: _xtest-owner@example.com + MIME-Version: 1.0 + Content-Type: multipart/mixed; boundary="..." + Message-ID: ... + Date: ... + Precedence: bulk + <BLANKLINE> + --... + Content-Type: text/plain; charset="us-ascii" + MIME-Version: 1.0 + Content-Transfer-Encoding: 7bit + <BLANKLINE> + As list administrator, your authorization is requested for the + following mailing list posting: + <BLANKLINE> + List: _xtest@example.com + From: aperson@example.com + Subject: (no subject) + Reason: Message has implicit destination + <BLANKLINE> + At your convenience, visit: + <BLANKLINE> + http://lists.example.com/admindb/_xtest@example.com + <BLANKLINE> + to approve or deny the request. + <BLANKLINE> + --... + Content-Type: message/rfc822 + MIME-Version: 1.0 + <BLANKLINE> + From: aperson@example.com + <BLANKLINE> + <BLANKLINE> + --... + Content-Type: message/rfc822 + MIME-Version: 1.0 + <BLANKLINE> + Content-Type: text/plain; charset="us-ascii" + MIME-Version: 1.0 + Content-Transfer-Encoding: 7bit + Subject: confirm ... + Sender: _xtest-request@example.com + From: _xtest-request@example.com + Date: ... + Message-ID: ... + <BLANKLINE> + If you reply to this message, keeping the Subject: header intact, + Mailman will discard the held message. Do this if the message is + spam. If you reply to this message and include an Approved: header + with the list password in it, the message will be approved for posting + to the list. The Approved: header can also appear in the first line + of the body of the reply. + --... + >>> sorted(qdata.items()) + [('_parsemsg', False), ('listname', '_xtest@example.com'), + ('nodecorate', True), ('received_time', ...), + ('recips', ['_xtest-owner@example.com']), + ('reduced_list_headers', True), + ('tomoderators', 1), ('version', 3)] + >>> qmsg, qdata = qfiles['aperson@example.com'] + >>> print qmsg.as_string() + MIME-Version: 1.0 + Content-Type: text/plain; charset="us-ascii" + Content-Transfer-Encoding: 7bit + Subject: Your message to _xtest awaits moderator approval + From: _xtest-bounces@example.com + To: aperson@example.com + Message-ID: ... + Date: ... + Precedence: bulk + <BLANKLINE> + Your mail to '_xtest' with the subject + <BLANKLINE> + (no subject) + <BLANKLINE> + Is being held until the list moderator can review it for approval. + <BLANKLINE> + The reason it is being held: + <BLANKLINE> + Message has implicit destination + <BLANKLINE> + Either the message will get posted to the list, or you will receive + notification of the moderator's decision. If you would like to cancel + this posting, please visit the following URL: + <BLANKLINE> + http://lists.example.com/confirm/_xtest@example.com/... + <BLANKLINE> + <BLANKLINE> + >>> sorted(qdata.items()) + [('_parsemsg', False), ('listname', '_xtest@example.com'), + ('nodecorate', True), ('received_time', ...), + ('recips', ['aperson@example.com']), + ('reduced_list_headers', True), ('version', 3)] + +In addition, the pending database is holding the original messages, waiting +for them to be disposed of by the original author or the list moderators. The +database is essentially a dictionary, with the keys being the randomly +selected tokens included in the urls and the values being a 2-tuple where the +first item is a type code and the second item is a message id. + + >>> import re + >>> cookie = None + >>> qmsg, qdata = qfiles['aperson@example.com'] + >>> for line in qmsg.get_payload().splitlines(): + ... mo = re.search('confirm/[^/]+/(?P<cookie>.*)$', line) + ... if mo: + ... cookie = mo.group('cookie') + ... break + >>> data = mlist.pend_confirm(cookie) + >>> data + ('H', ...) + >>> filename = 'heldmsg-_xtest@example.com-%d.pck' % data[1] + >>> heldmsg = os.path.join(config.DATA_DIR, filename) + +Use helper function to read the held message. + + >>> from Mailman.ListAdmin import readMessage + >>> msg = readMessage(heldmsg) + >>> print msg.as_string() + From: aperson@example.com + <BLANKLINE> + <BLANKLINE> + +Clean up. + + >>> clear() + >>> mlist.Unlock() |
