summaryrefslogtreecommitdiff
path: root/Mailman/docs/hold.txt
diff options
context:
space:
mode:
Diffstat (limited to 'Mailman/docs/hold.txt')
-rw-r--r--Mailman/docs/hold.txt373
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()