From 48354a7e6814190455fb566947ab952062ecde76 Mon Sep 17 00:00:00 2001 From: Barry Warsaw Date: Fri, 23 Sep 2011 21:42:39 -0400 Subject: Finally, all doctests are named .rst --- src/mailman/app/docs/bounces.rst | 101 +++++ src/mailman/app/docs/bounces.txt | 101 ----- src/mailman/app/docs/chains.rst | 343 ++++++++++++++ src/mailman/app/docs/chains.txt | 343 -------------- src/mailman/app/docs/hooks.rst | 113 +++++ src/mailman/app/docs/hooks.txt | 113 ----- src/mailman/app/docs/lifecycle.rst | 156 +++++++ src/mailman/app/docs/lifecycle.txt | 156 ------- src/mailman/app/docs/message.rst | 48 ++ src/mailman/app/docs/message.txt | 48 -- src/mailman/app/docs/pipelines.rst | 193 ++++++++ src/mailman/app/docs/pipelines.txt | 193 -------- src/mailman/app/docs/styles.rst | 162 +++++++ src/mailman/app/docs/styles.txt | 162 ------- src/mailman/app/docs/system.rst | 29 ++ src/mailman/app/docs/system.txt | 29 -- src/mailman/archiving/docs/common.rst | 180 ++++++++ src/mailman/archiving/docs/common.txt | 180 -------- src/mailman/commands/docs/echo.rst | 30 ++ src/mailman/commands/docs/echo.txt | 30 -- src/mailman/commands/docs/end.rst | 36 ++ src/mailman/commands/docs/end.txt | 36 -- src/mailman/commands/docs/import.rst | 56 +++ src/mailman/commands/docs/import.txt | 56 --- src/mailman/commands/docs/info.rst | 82 ++++ src/mailman/commands/docs/info.txt | 82 ---- src/mailman/commands/docs/lists.rst | 141 ++++++ src/mailman/commands/docs/lists.txt | 141 ------ src/mailman/commands/docs/members.rst | 322 +++++++++++++ src/mailman/commands/docs/members.txt | 322 ------------- src/mailman/commands/docs/membership.rst | 372 +++++++++++++++ src/mailman/commands/docs/membership.txt | 372 --------------- src/mailman/commands/docs/qfile.rst | 70 +++ src/mailman/commands/docs/qfile.txt | 70 --- src/mailman/commands/docs/remove.rst | 85 ++++ src/mailman/commands/docs/remove.txt | 85 ---- src/mailman/commands/docs/status.rst | 37 ++ src/mailman/commands/docs/status.txt | 37 -- src/mailman/commands/docs/unshunt.rst | 155 +++++++ src/mailman/commands/docs/unshunt.txt | 155 ------- src/mailman/commands/docs/version.rst | 12 + src/mailman/commands/docs/version.txt | 12 - src/mailman/commands/docs/withlist.rst | 125 +++++ src/mailman/commands/docs/withlist.txt | 125 ----- src/mailman/core/docs/switchboard.rst | 187 ++++++++ src/mailman/core/docs/switchboard.txt | 187 -------- src/mailman/docs/ACKNOWLEDGMENTS.rst | 268 +++++++++++ src/mailman/docs/ACKNOWLEDGMENTS.txt | 268 ----------- src/mailman/docs/MTA.rst | 129 ++++++ src/mailman/docs/MTA.txt | 129 ------ src/mailman/docs/README.rst | 117 +++++ src/mailman/docs/README.txt | 117 ----- src/mailman/docs/STYLEGUIDE.rst | 125 +++++ src/mailman/docs/STYLEGUIDE.txt | 125 ----- src/mailman/model/docs/addresses.rst | 204 +++++++++ src/mailman/model/docs/addresses.txt | 204 --------- src/mailman/model/docs/autorespond.rst | 116 +++++ src/mailman/model/docs/autorespond.txt | 116 ----- src/mailman/model/docs/languages.rst | 112 +++++ src/mailman/model/docs/languages.txt | 112 ----- src/mailman/model/docs/listmanager.rst | 99 ++++ src/mailman/model/docs/listmanager.txt | 99 ---- src/mailman/model/docs/mailinglist.rst | 165 +++++++ src/mailman/model/docs/mailinglist.txt | 165 ------- src/mailman/model/docs/messagestore.rst | 116 +++++ src/mailman/model/docs/messagestore.txt | 116 ----- src/mailman/model/docs/mlist-addresses.rst | 78 ++++ src/mailman/model/docs/mlist-addresses.txt | 78 ---- src/mailman/model/docs/registration.rst | 352 +++++++++++++++ src/mailman/model/docs/registration.txt | 352 --------------- src/mailman/mta/docs/authentication.rst | 68 +++ src/mailman/mta/docs/authentication.txt | 68 --- src/mailman/mta/docs/bulk.rst | 371 +++++++++++++++ src/mailman/mta/docs/bulk.txt | 371 --------------- src/mailman/mta/docs/connection.rst | 256 +++++++++++ src/mailman/mta/docs/connection.txt | 256 ----------- src/mailman/mta/docs/decorating.rst | 203 +++++++++ src/mailman/mta/docs/decorating.txt | 203 --------- src/mailman/mta/docs/personalized.rst | 192 ++++++++ src/mailman/mta/docs/personalized.txt | 192 -------- src/mailman/mta/docs/verp.rst | 134 ++++++ src/mailman/mta/docs/verp.txt | 134 ------ src/mailman/pipeline/docs/ack-headers.rst | 43 ++ src/mailman/pipeline/docs/ack-headers.txt | 43 -- src/mailman/pipeline/docs/acknowledge.rst | 178 ++++++++ src/mailman/pipeline/docs/acknowledge.txt | 178 -------- src/mailman/pipeline/docs/after-delivery.rst | 30 ++ src/mailman/pipeline/docs/after-delivery.txt | 30 -- src/mailman/pipeline/docs/archives.rst | 133 ++++++ src/mailman/pipeline/docs/archives.txt | 133 ------ src/mailman/pipeline/docs/avoid-duplicates.rst | 175 +++++++ src/mailman/pipeline/docs/avoid-duplicates.txt | 175 ------- src/mailman/pipeline/docs/calc-recips.rst | 114 +++++ src/mailman/pipeline/docs/calc-recips.txt | 114 ----- src/mailman/pipeline/docs/cleanse.rst | 100 ++++ src/mailman/pipeline/docs/cleanse.txt | 100 ---- src/mailman/pipeline/docs/cook-headers.rst | 332 ++++++++++++++ src/mailman/pipeline/docs/cook-headers.txt | 332 -------------- src/mailman/pipeline/docs/decorate.rst | 298 ++++++++++++ src/mailman/pipeline/docs/decorate.txt | 298 ------------ src/mailman/pipeline/docs/digests.rst | 113 +++++ src/mailman/pipeline/docs/digests.txt | 113 ----- src/mailman/pipeline/docs/file-recips.rst | 111 +++++ src/mailman/pipeline/docs/file-recips.txt | 111 ----- src/mailman/pipeline/docs/filtering.rst | 347 ++++++++++++++ src/mailman/pipeline/docs/filtering.txt | 347 -------------- src/mailman/pipeline/docs/nntp.rst | 68 +++ src/mailman/pipeline/docs/nntp.txt | 68 --- src/mailman/pipeline/docs/reply-to.rst | 131 ++++++ src/mailman/pipeline/docs/reply-to.txt | 131 ------ src/mailman/pipeline/docs/replybot.rst | 343 ++++++++++++++ src/mailman/pipeline/docs/replybot.txt | 343 -------------- src/mailman/pipeline/docs/scrubber.rst | 230 ++++++++++ src/mailman/pipeline/docs/scrubber.txt | 230 ---------- src/mailman/pipeline/docs/subject-munging.rst | 249 ++++++++++ src/mailman/pipeline/docs/subject-munging.txt | 249 ---------- src/mailman/pipeline/docs/tagger.rst | 238 ++++++++++ src/mailman/pipeline/docs/tagger.txt | 238 ---------- src/mailman/pipeline/docs/to-outgoing.rst | 42 ++ src/mailman/pipeline/docs/to-outgoing.txt | 42 -- src/mailman/rest/docs/basic.rst | 76 ++++ src/mailman/rest/docs/basic.txt | 76 ---- src/mailman/rest/docs/helpers.rst | 202 +++++++++ src/mailman/rest/docs/helpers.txt | 202 --------- src/mailman/rules/docs/administrivia.rst | 100 ++++ src/mailman/rules/docs/administrivia.txt | 100 ---- src/mailman/rules/docs/approve.rst | 514 +++++++++++++++++++++ src/mailman/rules/docs/approve.txt | 514 --------------------- src/mailman/rules/docs/emergency.rst | 37 ++ src/mailman/rules/docs/emergency.txt | 37 -- src/mailman/rules/docs/header-matching.rst | 146 ++++++ src/mailman/rules/docs/header-matching.txt | 146 ------ src/mailman/rules/docs/implicit-dest.rst | 129 ++++++ src/mailman/rules/docs/implicit-dest.txt | 129 ------ src/mailman/rules/docs/loop.rst | 49 ++ src/mailman/rules/docs/loop.txt | 49 -- src/mailman/rules/docs/max-size.rst | 40 ++ src/mailman/rules/docs/max-size.txt | 40 -- src/mailman/rules/docs/moderation.rst | 164 +++++++ src/mailman/rules/docs/moderation.txt | 164 ------- src/mailman/rules/docs/news-moderation.rst | 37 ++ src/mailman/rules/docs/news-moderation.txt | 37 -- src/mailman/rules/docs/no-subject.rst | 34 ++ src/mailman/rules/docs/no-subject.txt | 34 -- src/mailman/rules/docs/recipients.rst | 41 ++ src/mailman/rules/docs/recipients.txt | 41 -- src/mailman/rules/docs/rules.rst | 70 +++ src/mailman/rules/docs/rules.txt | 70 --- src/mailman/rules/docs/suspicious.rst | 36 ++ src/mailman/rules/docs/suspicious.txt | 36 -- src/mailman/rules/docs/truth.rst | 10 + src/mailman/rules/docs/truth.txt | 10 - src/mailman/runners/docs/OVERVIEW.rst | 80 ++++ src/mailman/runners/docs/OVERVIEW.txt | 80 ---- src/mailman/runners/docs/archiver.rst | 35 ++ src/mailman/runners/docs/archiver.txt | 35 -- src/mailman/runners/docs/command.rst | 289 ++++++++++++ src/mailman/runners/docs/command.txt | 289 ------------ src/mailman/runners/docs/digester.rst | 602 +++++++++++++++++++++++++ src/mailman/runners/docs/digester.txt | 602 ------------------------- src/mailman/runners/docs/incoming.rst | 263 +++++++++++ src/mailman/runners/docs/incoming.txt | 263 ----------- src/mailman/runners/docs/lmtp.rst | 313 +++++++++++++ src/mailman/runners/docs/lmtp.txt | 313 ------------- src/mailman/runners/docs/news.rst | 161 +++++++ src/mailman/runners/docs/news.txt | 161 ------- src/mailman/runners/docs/outgoing.rst | 413 +++++++++++++++++ src/mailman/runners/docs/outgoing.txt | 413 ----------------- src/mailman/runners/docs/rest.rst | 25 + src/mailman/runners/docs/rest.txt | 25 - 170 files changed, 13281 insertions(+), 13281 deletions(-) create mode 100644 src/mailman/app/docs/bounces.rst delete mode 100644 src/mailman/app/docs/bounces.txt create mode 100644 src/mailman/app/docs/chains.rst delete mode 100644 src/mailman/app/docs/chains.txt create mode 100644 src/mailman/app/docs/hooks.rst delete mode 100644 src/mailman/app/docs/hooks.txt create mode 100644 src/mailman/app/docs/lifecycle.rst delete mode 100644 src/mailman/app/docs/lifecycle.txt create mode 100644 src/mailman/app/docs/message.rst delete mode 100644 src/mailman/app/docs/message.txt create mode 100644 src/mailman/app/docs/pipelines.rst delete mode 100644 src/mailman/app/docs/pipelines.txt create mode 100644 src/mailman/app/docs/styles.rst delete mode 100644 src/mailman/app/docs/styles.txt create mode 100644 src/mailman/app/docs/system.rst delete mode 100644 src/mailman/app/docs/system.txt create mode 100644 src/mailman/archiving/docs/common.rst delete mode 100644 src/mailman/archiving/docs/common.txt create mode 100644 src/mailman/commands/docs/echo.rst delete mode 100644 src/mailman/commands/docs/echo.txt create mode 100644 src/mailman/commands/docs/end.rst delete mode 100644 src/mailman/commands/docs/end.txt create mode 100644 src/mailman/commands/docs/import.rst delete mode 100644 src/mailman/commands/docs/import.txt create mode 100644 src/mailman/commands/docs/info.rst delete mode 100644 src/mailman/commands/docs/info.txt create mode 100644 src/mailman/commands/docs/lists.rst delete mode 100644 src/mailman/commands/docs/lists.txt create mode 100644 src/mailman/commands/docs/members.rst delete mode 100644 src/mailman/commands/docs/members.txt create mode 100644 src/mailman/commands/docs/membership.rst delete mode 100644 src/mailman/commands/docs/membership.txt create mode 100644 src/mailman/commands/docs/qfile.rst delete mode 100644 src/mailman/commands/docs/qfile.txt create mode 100644 src/mailman/commands/docs/remove.rst delete mode 100644 src/mailman/commands/docs/remove.txt create mode 100644 src/mailman/commands/docs/status.rst delete mode 100644 src/mailman/commands/docs/status.txt create mode 100644 src/mailman/commands/docs/unshunt.rst delete mode 100644 src/mailman/commands/docs/unshunt.txt create mode 100644 src/mailman/commands/docs/version.rst delete mode 100644 src/mailman/commands/docs/version.txt create mode 100644 src/mailman/commands/docs/withlist.rst delete mode 100644 src/mailman/commands/docs/withlist.txt create mode 100644 src/mailman/core/docs/switchboard.rst delete mode 100644 src/mailman/core/docs/switchboard.txt create mode 100644 src/mailman/docs/ACKNOWLEDGMENTS.rst delete mode 100644 src/mailman/docs/ACKNOWLEDGMENTS.txt create mode 100644 src/mailman/docs/MTA.rst delete mode 100644 src/mailman/docs/MTA.txt create mode 100644 src/mailman/docs/README.rst delete mode 100644 src/mailman/docs/README.txt create mode 100644 src/mailman/docs/STYLEGUIDE.rst delete mode 100644 src/mailman/docs/STYLEGUIDE.txt create mode 100644 src/mailman/model/docs/addresses.rst delete mode 100644 src/mailman/model/docs/addresses.txt create mode 100644 src/mailman/model/docs/autorespond.rst delete mode 100644 src/mailman/model/docs/autorespond.txt create mode 100644 src/mailman/model/docs/languages.rst delete mode 100644 src/mailman/model/docs/languages.txt create mode 100644 src/mailman/model/docs/listmanager.rst delete mode 100644 src/mailman/model/docs/listmanager.txt create mode 100644 src/mailman/model/docs/mailinglist.rst delete mode 100644 src/mailman/model/docs/mailinglist.txt create mode 100644 src/mailman/model/docs/messagestore.rst delete mode 100644 src/mailman/model/docs/messagestore.txt create mode 100644 src/mailman/model/docs/mlist-addresses.rst delete mode 100644 src/mailman/model/docs/mlist-addresses.txt create mode 100644 src/mailman/model/docs/registration.rst delete mode 100644 src/mailman/model/docs/registration.txt create mode 100644 src/mailman/mta/docs/authentication.rst delete mode 100644 src/mailman/mta/docs/authentication.txt create mode 100644 src/mailman/mta/docs/bulk.rst delete mode 100644 src/mailman/mta/docs/bulk.txt create mode 100644 src/mailman/mta/docs/connection.rst delete mode 100644 src/mailman/mta/docs/connection.txt create mode 100644 src/mailman/mta/docs/decorating.rst delete mode 100644 src/mailman/mta/docs/decorating.txt create mode 100644 src/mailman/mta/docs/personalized.rst delete mode 100644 src/mailman/mta/docs/personalized.txt create mode 100644 src/mailman/mta/docs/verp.rst delete mode 100644 src/mailman/mta/docs/verp.txt create mode 100644 src/mailman/pipeline/docs/ack-headers.rst delete mode 100644 src/mailman/pipeline/docs/ack-headers.txt create mode 100644 src/mailman/pipeline/docs/acknowledge.rst delete mode 100644 src/mailman/pipeline/docs/acknowledge.txt create mode 100644 src/mailman/pipeline/docs/after-delivery.rst delete mode 100644 src/mailman/pipeline/docs/after-delivery.txt create mode 100644 src/mailman/pipeline/docs/archives.rst delete mode 100644 src/mailman/pipeline/docs/archives.txt create mode 100644 src/mailman/pipeline/docs/avoid-duplicates.rst delete mode 100644 src/mailman/pipeline/docs/avoid-duplicates.txt create mode 100644 src/mailman/pipeline/docs/calc-recips.rst delete mode 100644 src/mailman/pipeline/docs/calc-recips.txt create mode 100644 src/mailman/pipeline/docs/cleanse.rst delete mode 100644 src/mailman/pipeline/docs/cleanse.txt create mode 100644 src/mailman/pipeline/docs/cook-headers.rst delete mode 100644 src/mailman/pipeline/docs/cook-headers.txt create mode 100644 src/mailman/pipeline/docs/decorate.rst delete mode 100644 src/mailman/pipeline/docs/decorate.txt create mode 100644 src/mailman/pipeline/docs/digests.rst delete mode 100644 src/mailman/pipeline/docs/digests.txt create mode 100644 src/mailman/pipeline/docs/file-recips.rst delete mode 100644 src/mailman/pipeline/docs/file-recips.txt create mode 100644 src/mailman/pipeline/docs/filtering.rst delete mode 100644 src/mailman/pipeline/docs/filtering.txt create mode 100644 src/mailman/pipeline/docs/nntp.rst delete mode 100644 src/mailman/pipeline/docs/nntp.txt create mode 100644 src/mailman/pipeline/docs/reply-to.rst delete mode 100644 src/mailman/pipeline/docs/reply-to.txt create mode 100644 src/mailman/pipeline/docs/replybot.rst delete mode 100644 src/mailman/pipeline/docs/replybot.txt create mode 100644 src/mailman/pipeline/docs/scrubber.rst delete mode 100644 src/mailman/pipeline/docs/scrubber.txt create mode 100644 src/mailman/pipeline/docs/subject-munging.rst delete mode 100644 src/mailman/pipeline/docs/subject-munging.txt create mode 100644 src/mailman/pipeline/docs/tagger.rst delete mode 100644 src/mailman/pipeline/docs/tagger.txt create mode 100644 src/mailman/pipeline/docs/to-outgoing.rst delete mode 100644 src/mailman/pipeline/docs/to-outgoing.txt create mode 100644 src/mailman/rest/docs/basic.rst delete mode 100644 src/mailman/rest/docs/basic.txt create mode 100644 src/mailman/rest/docs/helpers.rst delete mode 100644 src/mailman/rest/docs/helpers.txt create mode 100644 src/mailman/rules/docs/administrivia.rst delete mode 100644 src/mailman/rules/docs/administrivia.txt create mode 100644 src/mailman/rules/docs/approve.rst delete mode 100644 src/mailman/rules/docs/approve.txt create mode 100644 src/mailman/rules/docs/emergency.rst delete mode 100644 src/mailman/rules/docs/emergency.txt create mode 100644 src/mailman/rules/docs/header-matching.rst delete mode 100644 src/mailman/rules/docs/header-matching.txt create mode 100644 src/mailman/rules/docs/implicit-dest.rst delete mode 100644 src/mailman/rules/docs/implicit-dest.txt create mode 100644 src/mailman/rules/docs/loop.rst delete mode 100644 src/mailman/rules/docs/loop.txt create mode 100644 src/mailman/rules/docs/max-size.rst delete mode 100644 src/mailman/rules/docs/max-size.txt create mode 100644 src/mailman/rules/docs/moderation.rst delete mode 100644 src/mailman/rules/docs/moderation.txt create mode 100644 src/mailman/rules/docs/news-moderation.rst delete mode 100644 src/mailman/rules/docs/news-moderation.txt create mode 100644 src/mailman/rules/docs/no-subject.rst delete mode 100644 src/mailman/rules/docs/no-subject.txt create mode 100644 src/mailman/rules/docs/recipients.rst delete mode 100644 src/mailman/rules/docs/recipients.txt create mode 100644 src/mailman/rules/docs/rules.rst delete mode 100644 src/mailman/rules/docs/rules.txt create mode 100644 src/mailman/rules/docs/suspicious.rst delete mode 100644 src/mailman/rules/docs/suspicious.txt create mode 100644 src/mailman/rules/docs/truth.rst delete mode 100644 src/mailman/rules/docs/truth.txt create mode 100644 src/mailman/runners/docs/OVERVIEW.rst delete mode 100644 src/mailman/runners/docs/OVERVIEW.txt create mode 100644 src/mailman/runners/docs/archiver.rst delete mode 100644 src/mailman/runners/docs/archiver.txt create mode 100644 src/mailman/runners/docs/command.rst delete mode 100644 src/mailman/runners/docs/command.txt create mode 100644 src/mailman/runners/docs/digester.rst delete mode 100644 src/mailman/runners/docs/digester.txt create mode 100644 src/mailman/runners/docs/incoming.rst delete mode 100644 src/mailman/runners/docs/incoming.txt create mode 100644 src/mailman/runners/docs/lmtp.rst delete mode 100644 src/mailman/runners/docs/lmtp.txt create mode 100644 src/mailman/runners/docs/news.rst delete mode 100644 src/mailman/runners/docs/news.txt create mode 100644 src/mailman/runners/docs/outgoing.rst delete mode 100644 src/mailman/runners/docs/outgoing.txt create mode 100644 src/mailman/runners/docs/rest.rst delete mode 100644 src/mailman/runners/docs/rest.txt diff --git a/src/mailman/app/docs/bounces.rst b/src/mailman/app/docs/bounces.rst new file mode 100644 index 000000000..f825064e3 --- /dev/null +++ b/src/mailman/app/docs/bounces.rst @@ -0,0 +1,101 @@ +======= +Bounces +======= + +An important feature of Mailman is automatic bounce process. + + +Bounces, or message rejection +============================= + +Mailman can bounce messages back to the original sender. This is essentially +equivalent to rejecting the message with notification. Mailing lists can +bounce a message with an optional error message. + + >>> mlist = create_list('_xtest@example.com') + +Any message can be bounced. + + >>> msg = message_from_string("""\ + ... To: _xtest@example.com + ... From: aperson@example.com + ... Subject: Something important + ... + ... I sometimes say something important. + ... """) + +Bounce a message by passing in the original message, and an optional error +message. The bounced message ends up in the virgin queue, awaiting sending +to the original message author. + + >>> from mailman.app.bounces import bounce_message + >>> bounce_message(mlist, msg) + >>> from mailman.testing.helpers import get_queue_messages + >>> items = get_queue_messages('virgin') + >>> len(items) + 1 + >>> print items[0].msg.as_string() + Subject: Something important + From: _xtest-owner@example.com + To: aperson@example.com + MIME-Version: 1.0 + Content-Type: multipart/mixed; boundary="..." + Message-ID: ... + Date: ... + Precedence: bulk + + --... + Content-Type: text/plain; charset="us-ascii" + MIME-Version: 1.0 + Content-Transfer-Encoding: 7bit + + [No bounce details are available] + --... + Content-Type: message/rfc822 + MIME-Version: 1.0 + + To: _xtest@example.com + From: aperson@example.com + Subject: Something important + + I sometimes say something important. + + --...-- + +An error message can be given when the message is bounced, and this will be +included in the payload of the text/plain part. The error message must be +passed in as an instance of a ``RejectMessage`` exception. + + >>> from mailman.core.errors import RejectMessage + >>> error = RejectMessage("This wasn't very important after all.") + >>> bounce_message(mlist, msg, error) + >>> items = get_queue_messages('virgin') + >>> len(items) + 1 + >>> print items[0].msg.as_string() + Subject: Something important + From: _xtest-owner@example.com + To: aperson@example.com + MIME-Version: 1.0 + Content-Type: multipart/mixed; boundary="..." + Message-ID: ... + Date: ... + Precedence: bulk + + --... + Content-Type: text/plain; charset="us-ascii" + MIME-Version: 1.0 + Content-Transfer-Encoding: 7bit + + This wasn't very important after all. + --... + Content-Type: message/rfc822 + MIME-Version: 1.0 + + To: _xtest@example.com + From: aperson@example.com + Subject: Something important + + I sometimes say something important. + + --...-- diff --git a/src/mailman/app/docs/bounces.txt b/src/mailman/app/docs/bounces.txt deleted file mode 100644 index f825064e3..000000000 --- a/src/mailman/app/docs/bounces.txt +++ /dev/null @@ -1,101 +0,0 @@ -======= -Bounces -======= - -An important feature of Mailman is automatic bounce process. - - -Bounces, or message rejection -============================= - -Mailman can bounce messages back to the original sender. This is essentially -equivalent to rejecting the message with notification. Mailing lists can -bounce a message with an optional error message. - - >>> mlist = create_list('_xtest@example.com') - -Any message can be bounced. - - >>> msg = message_from_string("""\ - ... To: _xtest@example.com - ... From: aperson@example.com - ... Subject: Something important - ... - ... I sometimes say something important. - ... """) - -Bounce a message by passing in the original message, and an optional error -message. The bounced message ends up in the virgin queue, awaiting sending -to the original message author. - - >>> from mailman.app.bounces import bounce_message - >>> bounce_message(mlist, msg) - >>> from mailman.testing.helpers import get_queue_messages - >>> items = get_queue_messages('virgin') - >>> len(items) - 1 - >>> print items[0].msg.as_string() - Subject: Something important - From: _xtest-owner@example.com - To: aperson@example.com - MIME-Version: 1.0 - Content-Type: multipart/mixed; boundary="..." - Message-ID: ... - Date: ... - Precedence: bulk - - --... - Content-Type: text/plain; charset="us-ascii" - MIME-Version: 1.0 - Content-Transfer-Encoding: 7bit - - [No bounce details are available] - --... - Content-Type: message/rfc822 - MIME-Version: 1.0 - - To: _xtest@example.com - From: aperson@example.com - Subject: Something important - - I sometimes say something important. - - --...-- - -An error message can be given when the message is bounced, and this will be -included in the payload of the text/plain part. The error message must be -passed in as an instance of a ``RejectMessage`` exception. - - >>> from mailman.core.errors import RejectMessage - >>> error = RejectMessage("This wasn't very important after all.") - >>> bounce_message(mlist, msg, error) - >>> items = get_queue_messages('virgin') - >>> len(items) - 1 - >>> print items[0].msg.as_string() - Subject: Something important - From: _xtest-owner@example.com - To: aperson@example.com - MIME-Version: 1.0 - Content-Type: multipart/mixed; boundary="..." - Message-ID: ... - Date: ... - Precedence: bulk - - --... - Content-Type: text/plain; charset="us-ascii" - MIME-Version: 1.0 - Content-Transfer-Encoding: 7bit - - This wasn't very important after all. - --... - Content-Type: message/rfc822 - MIME-Version: 1.0 - - To: _xtest@example.com - From: aperson@example.com - Subject: Something important - - I sometimes say something important. - - --...-- diff --git a/src/mailman/app/docs/chains.rst b/src/mailman/app/docs/chains.rst new file mode 100644 index 000000000..8a8ac0cc2 --- /dev/null +++ b/src/mailman/app/docs/chains.rst @@ -0,0 +1,343 @@ +====== +Chains +====== + +When a new message comes into the system, Mailman uses a set of rule chains to +decide whether the message gets posted to the list, rejected, discarded, or +held for moderator approval. + +There are a number of built-in chains available that act as end-points in the +processing of messages. + + +The Discard chain +================= + +The `discard` chain simply throws the message away. +:: + + >>> chain = config.chains['discard'] + >>> print chain.name + discard + >>> print chain.description + Discard a message and stop processing. + + >>> mlist = create_list('test@example.com') + >>> msg = message_from_string("""\ + ... From: aperson@example.com + ... To: test@example.com + ... Subject: My first post + ... Message-ID: + ... + ... An important message. + ... """) + + >>> def print_msgid(event): + ... print '{0}: {1}'.format( + ... event.chain.name.upper(), event.msg.get('message-id', 'n/a')) + + >>> from mailman.core.chains import process + >>> from mailman.testing.helpers import event_subscribers + + >>> with event_subscribers(print_msgid): + ... process(mlist, msg, {}, 'discard') + DISCARD: + + +The Reject chain +================ + +The `reject` chain bounces the message back to the original sender, and logs +this action. +:: + + >>> chain = config.chains['reject'] + >>> print chain.name + reject + >>> print chain.description + Reject/bounce a message and stop processing. + + >>> with event_subscribers(print_msgid): + ... process(mlist, msg, {}, 'reject') + REJECT: + +The bounce message is now sitting in the `virgin` queue. + + >>> from mailman.testing.helpers import get_queue_messages + >>> qfiles = get_queue_messages('virgin') + >>> len(qfiles) + 1 + >>> print qfiles[0].msg.as_string() + Subject: My first post + From: test-owner@example.com + To: aperson@example.com + ... + [No bounce details are available] + ... + Content-Type: message/rfc822 + MIME-Version: 1.0 + + From: aperson@example.com + To: test@example.com + Subject: My first post + Message-ID: + + An important message. + + ... + + +The Hold Chain +============== + +The `hold` chain places the message into the administrative request database +and depending on the list's settings, sends a notification to both the +original sender and the list moderators. :: + + >>> chain = config.chains['hold'] + >>> print chain.name + hold + >>> print chain.description + Hold a message and stop processing. + + >>> with event_subscribers(print_msgid): + ... process(mlist, msg, {}, 'hold') + HOLD: + +There are now two messages in the virgin queue, one to the list moderators and +one to the original author. + + >>> qfiles = get_queue_messages('virgin', sort_on='to') + >>> len(qfiles) + 2 + +One of the message is addressed to the mailing list moderators, and the other +is addressed to the original sender. + + >>> from operator import itemgetter + >>> messages = sorted((item.msg for item in qfiles), + ... key=itemgetter('to'), reverse=True) + +This one is addressed to the list moderators. + + >>> print messages[0].as_string() + Subject: test@example.com post from aperson@example.com requires approval + From: test-owner@example.com + To: test-owner@example.com + MIME-Version: 1.0 + ... + As list administrator, your authorization is requested for the + following mailing list posting: + + List: test@example.com + From: aperson@example.com + Subject: My first post + Reason: XXX + + At your convenience, visit: + + http://lists.example.com/admindb/test@example.com + + to approve or deny the request. + + ... + Content-Type: message/rfc822 + MIME-Version: 1.0 + + From: aperson@example.com + To: test@example.com + Subject: My first post + Message-ID: + X-Message-ID-Hash: RXJU4JL6N2OUN3OYMXXPPSCR7P7JE2BW + + An important message. + + ... + Content-Type: message/rfc822 + MIME-Version: 1.0 + + Content-Type: text/plain; charset="us-ascii" + MIME-Version: 1.0 + Content-Transfer-Encoding: 7bit + Subject: confirm ... + From: test-request@example.com + ... + + 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. + ... + +This message is addressed to the sender of the message. + + >>> print messages[1].as_string() + MIME-Version: 1.0 + Content-Type: text/plain; charset="us-ascii" + Content-Transfer-Encoding: 7bit + Subject: Your message to test@example.com awaits moderator approval + From: test-bounces@example.com + To: aperson@example.com + ... + Your mail to 'test@example.com' with the subject + + My first post + + Is being held until the list moderator can review it for approval. + + The reason it is being held: + + XXX + + 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: + + http://lists.example.com/confirm/test@example.com/... + + + +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 + >>> for line in messages[1].get_payload().splitlines(): + ... mo = re.search('confirm/[^/]+/(?P.*)$', line) + ... if mo: + ... cookie = mo.group('cookie') + ... break + >>> assert cookie is not None, 'No confirmation token found' + + >>> from mailman.interfaces.pending import IPendings + >>> from zope.component import getUtility + + >>> data = getUtility(IPendings).confirm(cookie) + >>> dump_msgdata(data) + id : 1 + type: held message + +The message itself is held in the message store. +:: + + >>> from mailman.interfaces.requests import IRequests + >>> list_requests = getUtility(IRequests).get_list_requests(mlist) + >>> rkey, rdata = list_requests.get_request(data['id']) + + >>> from mailman.interfaces.messages import IMessageStore + >>> from zope.component import getUtility + >>> msg = getUtility(IMessageStore).get_message_by_id( + ... rdata['_mod_message_id']) + + >>> print msg.as_string() + From: aperson@example.com + To: test@example.com + Subject: My first post + Message-ID: + X-Message-ID-Hash: RXJU4JL6N2OUN3OYMXXPPSCR7P7JE2BW + + An important message. + + + +The Accept chain +================ + +The `accept` chain sends the message on the `pipeline` queue, where it will be +processed and sent on to the list membership. +:: + + >>> chain = config.chains['accept'] + >>> print chain.name + accept + >>> print chain.description + Accept a message. + + >>> with event_subscribers(print_msgid): + ... process(mlist, msg, {}, 'accept') + ACCEPT: + + >>> qfiles = get_queue_messages('pipeline') + >>> len(qfiles) + 1 + >>> print qfiles[0].msg.as_string() + From: aperson@example.com + To: test@example.com + Subject: My first post + Message-ID: + X-Message-ID-Hash: RXJU4JL6N2OUN3OYMXXPPSCR7P7JE2BW + + An important message. + + + +Run-time chains +=============== + +We can also define chains at run time, and these chains can be mutated. +Run-time chains are made up of links where each link associates both a rule +and a `jump`. The rule is really a rule name, which is looked up when +needed. The jump names a chain which is jumped to if the rule matches. + +There is one built-in run-time chain, called appropriately `built-in`. This +is the default chain to use when no other input chain is defined for a mailing +list. It runs through the default rules. + + >>> chain = config.chains['built-in'] + >>> print chain.name + built-in + >>> print chain.description + The built-in moderation chain. + +Once the sender is a member of the mailing list, the previously created +message is innocuous enough that it should pass through all default rules. +This message will end up in the `pipeline` queue. +:: + + >>> from mailman.testing.helpers import subscribe + >>> subscribe(mlist, 'Anne') + + >>> with event_subscribers(print_msgid): + ... process(mlist, msg, {}) + ACCEPT: + + >>> qfiles = get_queue_messages('pipeline') + >>> len(qfiles) + 1 + >>> print qfiles[0].msg.as_string() + From: aperson@example.com + To: test@example.com + Subject: My first post + Message-ID: + X-Message-ID-Hash: RXJU4JL6N2OUN3OYMXXPPSCR7P7JE2BW + X-Mailman-Rule-Misses: approved; emergency; loop; member-moderation; + administrivia; implicit-dest; max-recipients; max-size; + news-moderation; no-subject; suspicious-header; nonmember-moderation + + An important message. + + +In addition, the message metadata now contains lists of all rules that have +hit and all rules that have missed. + + >>> dump_list(qfiles[0].msgdata['rule_hits']) + *Empty* + >>> dump_list(qfiles[0].msgdata['rule_misses']) + administrivia + approved + emergency + implicit-dest + loop + max-recipients + max-size + member-moderation + news-moderation + no-subject + nonmember-moderation + suspicious-header diff --git a/src/mailman/app/docs/chains.txt b/src/mailman/app/docs/chains.txt deleted file mode 100644 index 8a8ac0cc2..000000000 --- a/src/mailman/app/docs/chains.txt +++ /dev/null @@ -1,343 +0,0 @@ -====== -Chains -====== - -When a new message comes into the system, Mailman uses a set of rule chains to -decide whether the message gets posted to the list, rejected, discarded, or -held for moderator approval. - -There are a number of built-in chains available that act as end-points in the -processing of messages. - - -The Discard chain -================= - -The `discard` chain simply throws the message away. -:: - - >>> chain = config.chains['discard'] - >>> print chain.name - discard - >>> print chain.description - Discard a message and stop processing. - - >>> mlist = create_list('test@example.com') - >>> msg = message_from_string("""\ - ... From: aperson@example.com - ... To: test@example.com - ... Subject: My first post - ... Message-ID: - ... - ... An important message. - ... """) - - >>> def print_msgid(event): - ... print '{0}: {1}'.format( - ... event.chain.name.upper(), event.msg.get('message-id', 'n/a')) - - >>> from mailman.core.chains import process - >>> from mailman.testing.helpers import event_subscribers - - >>> with event_subscribers(print_msgid): - ... process(mlist, msg, {}, 'discard') - DISCARD: - - -The Reject chain -================ - -The `reject` chain bounces the message back to the original sender, and logs -this action. -:: - - >>> chain = config.chains['reject'] - >>> print chain.name - reject - >>> print chain.description - Reject/bounce a message and stop processing. - - >>> with event_subscribers(print_msgid): - ... process(mlist, msg, {}, 'reject') - REJECT: - -The bounce message is now sitting in the `virgin` queue. - - >>> from mailman.testing.helpers import get_queue_messages - >>> qfiles = get_queue_messages('virgin') - >>> len(qfiles) - 1 - >>> print qfiles[0].msg.as_string() - Subject: My first post - From: test-owner@example.com - To: aperson@example.com - ... - [No bounce details are available] - ... - Content-Type: message/rfc822 - MIME-Version: 1.0 - - From: aperson@example.com - To: test@example.com - Subject: My first post - Message-ID: - - An important message. - - ... - - -The Hold Chain -============== - -The `hold` chain places the message into the administrative request database -and depending on the list's settings, sends a notification to both the -original sender and the list moderators. :: - - >>> chain = config.chains['hold'] - >>> print chain.name - hold - >>> print chain.description - Hold a message and stop processing. - - >>> with event_subscribers(print_msgid): - ... process(mlist, msg, {}, 'hold') - HOLD: - -There are now two messages in the virgin queue, one to the list moderators and -one to the original author. - - >>> qfiles = get_queue_messages('virgin', sort_on='to') - >>> len(qfiles) - 2 - -One of the message is addressed to the mailing list moderators, and the other -is addressed to the original sender. - - >>> from operator import itemgetter - >>> messages = sorted((item.msg for item in qfiles), - ... key=itemgetter('to'), reverse=True) - -This one is addressed to the list moderators. - - >>> print messages[0].as_string() - Subject: test@example.com post from aperson@example.com requires approval - From: test-owner@example.com - To: test-owner@example.com - MIME-Version: 1.0 - ... - As list administrator, your authorization is requested for the - following mailing list posting: - - List: test@example.com - From: aperson@example.com - Subject: My first post - Reason: XXX - - At your convenience, visit: - - http://lists.example.com/admindb/test@example.com - - to approve or deny the request. - - ... - Content-Type: message/rfc822 - MIME-Version: 1.0 - - From: aperson@example.com - To: test@example.com - Subject: My first post - Message-ID: - X-Message-ID-Hash: RXJU4JL6N2OUN3OYMXXPPSCR7P7JE2BW - - An important message. - - ... - Content-Type: message/rfc822 - MIME-Version: 1.0 - - Content-Type: text/plain; charset="us-ascii" - MIME-Version: 1.0 - Content-Transfer-Encoding: 7bit - Subject: confirm ... - From: test-request@example.com - ... - - 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. - ... - -This message is addressed to the sender of the message. - - >>> print messages[1].as_string() - MIME-Version: 1.0 - Content-Type: text/plain; charset="us-ascii" - Content-Transfer-Encoding: 7bit - Subject: Your message to test@example.com awaits moderator approval - From: test-bounces@example.com - To: aperson@example.com - ... - Your mail to 'test@example.com' with the subject - - My first post - - Is being held until the list moderator can review it for approval. - - The reason it is being held: - - XXX - - 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: - - http://lists.example.com/confirm/test@example.com/... - - - -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 - >>> for line in messages[1].get_payload().splitlines(): - ... mo = re.search('confirm/[^/]+/(?P.*)$', line) - ... if mo: - ... cookie = mo.group('cookie') - ... break - >>> assert cookie is not None, 'No confirmation token found' - - >>> from mailman.interfaces.pending import IPendings - >>> from zope.component import getUtility - - >>> data = getUtility(IPendings).confirm(cookie) - >>> dump_msgdata(data) - id : 1 - type: held message - -The message itself is held in the message store. -:: - - >>> from mailman.interfaces.requests import IRequests - >>> list_requests = getUtility(IRequests).get_list_requests(mlist) - >>> rkey, rdata = list_requests.get_request(data['id']) - - >>> from mailman.interfaces.messages import IMessageStore - >>> from zope.component import getUtility - >>> msg = getUtility(IMessageStore).get_message_by_id( - ... rdata['_mod_message_id']) - - >>> print msg.as_string() - From: aperson@example.com - To: test@example.com - Subject: My first post - Message-ID: - X-Message-ID-Hash: RXJU4JL6N2OUN3OYMXXPPSCR7P7JE2BW - - An important message. - - - -The Accept chain -================ - -The `accept` chain sends the message on the `pipeline` queue, where it will be -processed and sent on to the list membership. -:: - - >>> chain = config.chains['accept'] - >>> print chain.name - accept - >>> print chain.description - Accept a message. - - >>> with event_subscribers(print_msgid): - ... process(mlist, msg, {}, 'accept') - ACCEPT: - - >>> qfiles = get_queue_messages('pipeline') - >>> len(qfiles) - 1 - >>> print qfiles[0].msg.as_string() - From: aperson@example.com - To: test@example.com - Subject: My first post - Message-ID: - X-Message-ID-Hash: RXJU4JL6N2OUN3OYMXXPPSCR7P7JE2BW - - An important message. - - - -Run-time chains -=============== - -We can also define chains at run time, and these chains can be mutated. -Run-time chains are made up of links where each link associates both a rule -and a `jump`. The rule is really a rule name, which is looked up when -needed. The jump names a chain which is jumped to if the rule matches. - -There is one built-in run-time chain, called appropriately `built-in`. This -is the default chain to use when no other input chain is defined for a mailing -list. It runs through the default rules. - - >>> chain = config.chains['built-in'] - >>> print chain.name - built-in - >>> print chain.description - The built-in moderation chain. - -Once the sender is a member of the mailing list, the previously created -message is innocuous enough that it should pass through all default rules. -This message will end up in the `pipeline` queue. -:: - - >>> from mailman.testing.helpers import subscribe - >>> subscribe(mlist, 'Anne') - - >>> with event_subscribers(print_msgid): - ... process(mlist, msg, {}) - ACCEPT: - - >>> qfiles = get_queue_messages('pipeline') - >>> len(qfiles) - 1 - >>> print qfiles[0].msg.as_string() - From: aperson@example.com - To: test@example.com - Subject: My first post - Message-ID: - X-Message-ID-Hash: RXJU4JL6N2OUN3OYMXXPPSCR7P7JE2BW - X-Mailman-Rule-Misses: approved; emergency; loop; member-moderation; - administrivia; implicit-dest; max-recipients; max-size; - news-moderation; no-subject; suspicious-header; nonmember-moderation - - An important message. - - -In addition, the message metadata now contains lists of all rules that have -hit and all rules that have missed. - - >>> dump_list(qfiles[0].msgdata['rule_hits']) - *Empty* - >>> dump_list(qfiles[0].msgdata['rule_misses']) - administrivia - approved - emergency - implicit-dest - loop - max-recipients - max-size - member-moderation - news-moderation - no-subject - nonmember-moderation - suspicious-header diff --git a/src/mailman/app/docs/hooks.rst b/src/mailman/app/docs/hooks.rst new file mode 100644 index 000000000..7e214f13f --- /dev/null +++ b/src/mailman/app/docs/hooks.rst @@ -0,0 +1,113 @@ +===== +Hooks +===== + +Mailman defines two initialization hooks, one which is run early in the +initialization process and the other run late in the initialization process. +Hooks name an importable callable so it must be accessible on ``sys.path``. +:: + + >>> import os, sys + >>> from mailman.config import config + >>> config_directory = os.path.dirname(config.filename) + >>> sys.path.insert(0, config_directory) + + >>> hook_path = os.path.join(config_directory, 'hooks.py') + >>> with open(hook_path, 'w') as fp: + ... print >> fp, """\ + ... counter = 1 + ... def pre_hook(): + ... global counter + ... print 'pre-hook:', counter + ... counter += 1 + ... + ... def post_hook(): + ... global counter + ... print 'post-hook:', counter + ... counter += 1 + ... """ + >>> fp.close() + + +Pre-hook +======== + +We can set the pre-hook in the configuration file. + + >>> config_path = os.path.join(config_directory, 'hooks.cfg') + >>> with open(config_path, 'w') as fp: + ... print >> fp, """\ + ... [meta] + ... extends: test.cfg + ... + ... [mailman] + ... pre_hook: hooks.pre_hook + ... """ + +The hooks are run in the second and third steps of initialization. However, +we can't run those initialization steps in process, so call a command line +script that will produce no output to force the hooks to run. +:: + + >>> import subprocess + >>> from mailman.testing.layers import ConfigLayer + >>> def call(): + ... proc = subprocess.Popen( + ... 'bin/mailman lists --domain ignore -q'.split(), + ... cwd=ConfigLayer.root_directory, + ... env=dict(MAILMAN_CONFIG_FILE=config_path, + ... PYTHONPATH=config_directory), + ... stdout=subprocess.PIPE, stderr=subprocess.PIPE) + ... stdout, stderr = proc.communicate() + ... assert proc.returncode == 0, stderr + ... print stdout + + >>> call() + pre-hook: 1 + + + >>> os.remove(config_path) + + +Post-hook +========= + +We can set the post-hook in the configuration file. +:: + + >>> with open(config_path, 'w') as fp: + ... print >> fp, """\ + ... [meta] + ... extends: test.cfg + ... + ... [mailman] + ... post_hook: hooks.post_hook + ... """ + + >>> call() + post-hook: 1 + + + >>> os.remove(config_path) + + +Running both hooks +================== + +We can set the pre- and post-hooks in the configuration file. +:: + + >>> with open(config_path, 'w') as fp: + ... print >> fp, """\ + ... [meta] + ... extends: test.cfg + ... + ... [mailman] + ... pre_hook: hooks.pre_hook + ... post_hook: hooks.post_hook + ... """ + + >>> call() + pre-hook: 1 + post-hook: 2 + diff --git a/src/mailman/app/docs/hooks.txt b/src/mailman/app/docs/hooks.txt deleted file mode 100644 index 7e214f13f..000000000 --- a/src/mailman/app/docs/hooks.txt +++ /dev/null @@ -1,113 +0,0 @@ -===== -Hooks -===== - -Mailman defines two initialization hooks, one which is run early in the -initialization process and the other run late in the initialization process. -Hooks name an importable callable so it must be accessible on ``sys.path``. -:: - - >>> import os, sys - >>> from mailman.config import config - >>> config_directory = os.path.dirname(config.filename) - >>> sys.path.insert(0, config_directory) - - >>> hook_path = os.path.join(config_directory, 'hooks.py') - >>> with open(hook_path, 'w') as fp: - ... print >> fp, """\ - ... counter = 1 - ... def pre_hook(): - ... global counter - ... print 'pre-hook:', counter - ... counter += 1 - ... - ... def post_hook(): - ... global counter - ... print 'post-hook:', counter - ... counter += 1 - ... """ - >>> fp.close() - - -Pre-hook -======== - -We can set the pre-hook in the configuration file. - - >>> config_path = os.path.join(config_directory, 'hooks.cfg') - >>> with open(config_path, 'w') as fp: - ... print >> fp, """\ - ... [meta] - ... extends: test.cfg - ... - ... [mailman] - ... pre_hook: hooks.pre_hook - ... """ - -The hooks are run in the second and third steps of initialization. However, -we can't run those initialization steps in process, so call a command line -script that will produce no output to force the hooks to run. -:: - - >>> import subprocess - >>> from mailman.testing.layers import ConfigLayer - >>> def call(): - ... proc = subprocess.Popen( - ... 'bin/mailman lists --domain ignore -q'.split(), - ... cwd=ConfigLayer.root_directory, - ... env=dict(MAILMAN_CONFIG_FILE=config_path, - ... PYTHONPATH=config_directory), - ... stdout=subprocess.PIPE, stderr=subprocess.PIPE) - ... stdout, stderr = proc.communicate() - ... assert proc.returncode == 0, stderr - ... print stdout - - >>> call() - pre-hook: 1 - - - >>> os.remove(config_path) - - -Post-hook -========= - -We can set the post-hook in the configuration file. -:: - - >>> with open(config_path, 'w') as fp: - ... print >> fp, """\ - ... [meta] - ... extends: test.cfg - ... - ... [mailman] - ... post_hook: hooks.post_hook - ... """ - - >>> call() - post-hook: 1 - - - >>> os.remove(config_path) - - -Running both hooks -================== - -We can set the pre- and post-hooks in the configuration file. -:: - - >>> with open(config_path, 'w') as fp: - ... print >> fp, """\ - ... [meta] - ... extends: test.cfg - ... - ... [mailman] - ... pre_hook: hooks.pre_hook - ... post_hook: hooks.post_hook - ... """ - - >>> call() - pre-hook: 1 - post-hook: 2 - diff --git a/src/mailman/app/docs/lifecycle.rst b/src/mailman/app/docs/lifecycle.rst new file mode 100644 index 000000000..4a8b732e1 --- /dev/null +++ b/src/mailman/app/docs/lifecycle.rst @@ -0,0 +1,156 @@ +================================= +Application level list life cycle +================================= + +The low-level way to create and delete a mailing list is to use the +``IListManager`` interface. This interface simply adds or removes the +appropriate database entries to record the list's creation. + +There is a higher level interface for creating and deleting mailing lists +which performs additional tasks such as: + + * validating the list's posting address (which also serves as the list's + fully qualified name); + * ensuring that the list's domain is registered; + * applying all matching styles to the new list; + * creating and assigning list owners; + * notifying watchers of list creation; + * creating ancillary artifacts (such as the list's on-disk directory) + + +Posting address validation +========================== + +If you try to use the higher-level interface to create a mailing list with a +bogus posting address, you get an exception. + + >>> create_list('not a valid address') + Traceback (most recent call last): + ... + InvalidEmailAddressError: not a valid address + +If the posting address is valid, but the domain has not been registered with +Mailman yet, you get an exception. + + >>> create_list('test@example.org') + Traceback (most recent call last): + ... + BadDomainSpecificationError: example.org + + +Creating a list applies its styles +================================== + +Start by registering a test style. +:: + + >>> from zope.interface import implements + >>> from mailman.interfaces.styles import IStyle + >>> class TestStyle(object): + ... implements(IStyle) + ... name = 'test' + ... priority = 10 + ... def apply(self, mailing_list): + ... # Just does something very simple. + ... mailing_list.msg_footer = 'test footer' + ... def match(self, mailing_list, styles): + ... # Applies to any test list + ... if 'test' in mailing_list.fqdn_listname: + ... styles.append(self) + + >>> config.style_manager.register(TestStyle()) + +Using the higher level interface for creating a list, applies all matching +list styles. + + >>> mlist_1 = create_list('test_1@example.com') + >>> print mlist_1.fqdn_listname + test_1@example.com + >>> print mlist_1.msg_footer + test footer + + +Creating a list with owners +=========================== + +You can also specify a list of owner email addresses. If these addresses are +not yet known, they will be registered, and new users will be linked to them. +However the addresses are not verified. + + >>> owners = [ + ... 'aperson@example.com', + ... 'bperson@example.com', + ... 'cperson@example.com', + ... 'dperson@example.com', + ... ] + >>> mlist_2 = create_list('test_2@example.com', owners) + >>> print mlist_2.fqdn_listname + test_2@example.com + >>> print mlist_2.msg_footer + test footer + >>> dump_list(address.email for address in mlist_2.owners.addresses) + aperson@example.com + bperson@example.com + cperson@example.com + dperson@example.com + +None of the owner addresses are verified. + + >>> any(address.verified_on is not None + ... for address in mlist_2.owners.addresses) + False + +However, all addresses are linked to users. + + >>> # The owners have no names yet + >>> len(list(mlist_2.owners.users)) + 4 + +If you create a mailing list with owner addresses that are already known to +the system, they won't be created again. +:: + + >>> from mailman.interfaces.usermanager import IUserManager + >>> from zope.component import getUtility + >>> user_manager = getUtility(IUserManager) + + >>> user_a = user_manager.get_user('aperson@example.com') + >>> user_b = user_manager.get_user('bperson@example.com') + >>> user_c = user_manager.get_user('cperson@example.com') + >>> user_d = user_manager.get_user('dperson@example.com') + >>> user_a.real_name = 'Anne Person' + >>> user_b.real_name = 'Bart Person' + >>> user_c.real_name = 'Caty Person' + >>> user_d.real_name = 'Dirk Person' + + >>> mlist_3 = create_list('test_3@example.com', owners) + >>> dump_list(user.real_name for user in mlist_3.owners.users) + Anne Person + Bart Person + Caty Person + Dirk Person + + +Deleting a list +=============== + +Removing a mailing list deletes the list, all its subscribers, and any related +artifacts. +:: + + >>> from mailman.app.lifecycle import remove_list + >>> remove_list(mlist_2.fqdn_listname, mlist_2, True) + + >>> from mailman.interfaces.listmanager import IListManager + >>> from zope.component import getUtility + >>> print getUtility(IListManager).get('test_2@example.com') + None + +We should now be able to completely recreate the mailing list. + + >>> mlist_2a = create_list('test_2@example.com', owners) + >>> dump_list(address.email for address in mlist_2a.owners.addresses) + aperson@example.com + bperson@example.com + cperson@example.com + dperson@example.com diff --git a/src/mailman/app/docs/lifecycle.txt b/src/mailman/app/docs/lifecycle.txt deleted file mode 100644 index 4a8b732e1..000000000 --- a/src/mailman/app/docs/lifecycle.txt +++ /dev/null @@ -1,156 +0,0 @@ -================================= -Application level list life cycle -================================= - -The low-level way to create and delete a mailing list is to use the -``IListManager`` interface. This interface simply adds or removes the -appropriate database entries to record the list's creation. - -There is a higher level interface for creating and deleting mailing lists -which performs additional tasks such as: - - * validating the list's posting address (which also serves as the list's - fully qualified name); - * ensuring that the list's domain is registered; - * applying all matching styles to the new list; - * creating and assigning list owners; - * notifying watchers of list creation; - * creating ancillary artifacts (such as the list's on-disk directory) - - -Posting address validation -========================== - -If you try to use the higher-level interface to create a mailing list with a -bogus posting address, you get an exception. - - >>> create_list('not a valid address') - Traceback (most recent call last): - ... - InvalidEmailAddressError: not a valid address - -If the posting address is valid, but the domain has not been registered with -Mailman yet, you get an exception. - - >>> create_list('test@example.org') - Traceback (most recent call last): - ... - BadDomainSpecificationError: example.org - - -Creating a list applies its styles -================================== - -Start by registering a test style. -:: - - >>> from zope.interface import implements - >>> from mailman.interfaces.styles import IStyle - >>> class TestStyle(object): - ... implements(IStyle) - ... name = 'test' - ... priority = 10 - ... def apply(self, mailing_list): - ... # Just does something very simple. - ... mailing_list.msg_footer = 'test footer' - ... def match(self, mailing_list, styles): - ... # Applies to any test list - ... if 'test' in mailing_list.fqdn_listname: - ... styles.append(self) - - >>> config.style_manager.register(TestStyle()) - -Using the higher level interface for creating a list, applies all matching -list styles. - - >>> mlist_1 = create_list('test_1@example.com') - >>> print mlist_1.fqdn_listname - test_1@example.com - >>> print mlist_1.msg_footer - test footer - - -Creating a list with owners -=========================== - -You can also specify a list of owner email addresses. If these addresses are -not yet known, they will be registered, and new users will be linked to them. -However the addresses are not verified. - - >>> owners = [ - ... 'aperson@example.com', - ... 'bperson@example.com', - ... 'cperson@example.com', - ... 'dperson@example.com', - ... ] - >>> mlist_2 = create_list('test_2@example.com', owners) - >>> print mlist_2.fqdn_listname - test_2@example.com - >>> print mlist_2.msg_footer - test footer - >>> dump_list(address.email for address in mlist_2.owners.addresses) - aperson@example.com - bperson@example.com - cperson@example.com - dperson@example.com - -None of the owner addresses are verified. - - >>> any(address.verified_on is not None - ... for address in mlist_2.owners.addresses) - False - -However, all addresses are linked to users. - - >>> # The owners have no names yet - >>> len(list(mlist_2.owners.users)) - 4 - -If you create a mailing list with owner addresses that are already known to -the system, they won't be created again. -:: - - >>> from mailman.interfaces.usermanager import IUserManager - >>> from zope.component import getUtility - >>> user_manager = getUtility(IUserManager) - - >>> user_a = user_manager.get_user('aperson@example.com') - >>> user_b = user_manager.get_user('bperson@example.com') - >>> user_c = user_manager.get_user('cperson@example.com') - >>> user_d = user_manager.get_user('dperson@example.com') - >>> user_a.real_name = 'Anne Person' - >>> user_b.real_name = 'Bart Person' - >>> user_c.real_name = 'Caty Person' - >>> user_d.real_name = 'Dirk Person' - - >>> mlist_3 = create_list('test_3@example.com', owners) - >>> dump_list(user.real_name for user in mlist_3.owners.users) - Anne Person - Bart Person - Caty Person - Dirk Person - - -Deleting a list -=============== - -Removing a mailing list deletes the list, all its subscribers, and any related -artifacts. -:: - - >>> from mailman.app.lifecycle import remove_list - >>> remove_list(mlist_2.fqdn_listname, mlist_2, True) - - >>> from mailman.interfaces.listmanager import IListManager - >>> from zope.component import getUtility - >>> print getUtility(IListManager).get('test_2@example.com') - None - -We should now be able to completely recreate the mailing list. - - >>> mlist_2a = create_list('test_2@example.com', owners) - >>> dump_list(address.email for address in mlist_2a.owners.addresses) - aperson@example.com - bperson@example.com - cperson@example.com - dperson@example.com diff --git a/src/mailman/app/docs/message.rst b/src/mailman/app/docs/message.rst new file mode 100644 index 000000000..3e3293196 --- /dev/null +++ b/src/mailman/app/docs/message.rst @@ -0,0 +1,48 @@ +======== +Messages +======== + +Mailman has its own Message classes, derived from the standard +``email.message.Message`` class, but providing additional useful methods. + + +User notifications +================== + +When Mailman needs to send a message to a user, it creates a +``UserNotification`` instance, and then calls the ``.send()`` method on this +object. This method requires a mailing list instance. + + >>> mlist = create_list('_xtest@example.com') + +The ``UserNotification`` constructor takes the recipient address, the sender +address, an optional subject, optional body text, and optional language. + + >>> from mailman.email.message import UserNotification + >>> msg = UserNotification( + ... 'aperson@example.com', + ... '_xtest@example.com', + ... 'Something you need to know', + ... 'I needed to tell you this.') + >>> msg.send(mlist) + +The message will end up in the `virgin` queue. + + >>> switchboard = config.switchboards['virgin'] + >>> len(switchboard.files) + 1 + >>> filebase = switchboard.files[0] + >>> qmsg, qmsgdata = switchboard.dequeue(filebase) + >>> switchboard.finish(filebase) + >>> print qmsg.as_string() + MIME-Version: 1.0 + Content-Type: text/plain; charset="us-ascii" + Content-Transfer-Encoding: 7bit + Subject: Something you need to know + From: _xtest@example.com + To: aperson@example.com + Message-ID: ... + Date: ... + Precedence: bulk + + I needed to tell you this. diff --git a/src/mailman/app/docs/message.txt b/src/mailman/app/docs/message.txt deleted file mode 100644 index 3e3293196..000000000 --- a/src/mailman/app/docs/message.txt +++ /dev/null @@ -1,48 +0,0 @@ -======== -Messages -======== - -Mailman has its own Message classes, derived from the standard -``email.message.Message`` class, but providing additional useful methods. - - -User notifications -================== - -When Mailman needs to send a message to a user, it creates a -``UserNotification`` instance, and then calls the ``.send()`` method on this -object. This method requires a mailing list instance. - - >>> mlist = create_list('_xtest@example.com') - -The ``UserNotification`` constructor takes the recipient address, the sender -address, an optional subject, optional body text, and optional language. - - >>> from mailman.email.message import UserNotification - >>> msg = UserNotification( - ... 'aperson@example.com', - ... '_xtest@example.com', - ... 'Something you need to know', - ... 'I needed to tell you this.') - >>> msg.send(mlist) - -The message will end up in the `virgin` queue. - - >>> switchboard = config.switchboards['virgin'] - >>> len(switchboard.files) - 1 - >>> filebase = switchboard.files[0] - >>> qmsg, qmsgdata = switchboard.dequeue(filebase) - >>> switchboard.finish(filebase) - >>> print qmsg.as_string() - MIME-Version: 1.0 - Content-Type: text/plain; charset="us-ascii" - Content-Transfer-Encoding: 7bit - Subject: Something you need to know - From: _xtest@example.com - To: aperson@example.com - Message-ID: ... - Date: ... - Precedence: bulk - - I needed to tell you this. diff --git a/src/mailman/app/docs/pipelines.rst b/src/mailman/app/docs/pipelines.rst new file mode 100644 index 000000000..cf848f1d9 --- /dev/null +++ b/src/mailman/app/docs/pipelines.rst @@ -0,0 +1,193 @@ +========= +Pipelines +========= + +This runner's purpose in life is to process messages that have been accepted +for posting, applying any modifications and also sending copies of the message +to the archives, digests, nntp, and outgoing queues. Pipelines are named and +consist of a sequence of handlers, each of which is applied in turn. Unlike +rules and chains, there is no way to stop a pipeline from processing the +message once it's started. + + >>> mlist = create_list('xtest@example.com') + >>> print mlist.pipeline + built-in + >>> from mailman.core.pipelines import process + + +Processing a message +==================== + +Messages hit the pipeline after they've been accepted for posting. + + >>> msg = message_from_string("""\ + ... From: aperson@example.com + ... To: xtest@example.com + ... Subject: My first post + ... Message-ID: + ... + ... First post! + ... """) + >>> msgdata = {} + >>> process(mlist, msg, msgdata, mlist.pipeline) + +The message has been modified with additional headers, footer decorations, +etc. + + >>> print msg.as_string() + From: aperson@example.com + To: xtest@example.com + Message-ID: + Subject: [Xtest] My first post + X-BeenThere: xtest@example.com + X-Mailman-Version: ... + Precedence: list + List-Id: + X-Message-ID-Hash: 4CMWUN6BHVCMHMDAOSJZ2Q72G5M32MWB + List-Post: + List-Subscribe: + , + + Archived-At: + http://lists.example.com/archives/4CMWUN6BHVCMHMDAOSJZ2Q72G5M32MWB + List-Unsubscribe: + , + + List-Archive: + List-Help: + + First post! + + +And the message metadata has information about recipients and other stuff. +However there are currently no recipients for this message. + + >>> dump_msgdata(msgdata) + original_sender : aperson@example.com + origsubj : My first post + recipients : set([]) + stripped_subject: My first post + +And the message is now sitting in various other processing queues. + + >>> from mailman.testing.helpers import get_queue_messages + >>> messages = get_queue_messages('archive') + >>> len(messages) + 1 + >>> print messages[0].msg.as_string() + From: aperson@example.com + To: xtest@example.com + Message-ID: + Subject: [Xtest] My first post + X-BeenThere: xtest@example.com + X-Mailman-Version: ... + Precedence: list + List-Id: + X-Message-ID-Hash: 4CMWUN6BHVCMHMDAOSJZ2Q72G5M32MWB + List-Post: + List-Subscribe: + , + + Archived-At: + http://lists.example.com/archives/4CMWUN6BHVCMHMDAOSJZ2Q72G5M32MWB + List-Unsubscribe: + , + + List-Archive: + List-Help: + + First post! + + >>> dump_msgdata(messages[0].msgdata) + _parsemsg : False + original_sender : aperson@example.com + origsubj : My first post + recipients : set([]) + stripped_subject: My first post + version : 3 + +This mailing list is not linked to an NNTP newsgroup, so there's nothing in +the outgoing nntp queue. + + >>> messages = get_queue_messages('news') + >>> len(messages) + 0 + +This is the message that will actually get delivered to end recipients. + + >>> messages = get_queue_messages('out') + >>> len(messages) + 1 + >>> print messages[0].msg.as_string() + From: aperson@example.com + To: xtest@example.com + Message-ID: + Subject: [Xtest] My first post + X-BeenThere: xtest@example.com + X-Mailman-Version: ... + Precedence: list + List-Id: + X-Message-ID-Hash: 4CMWUN6BHVCMHMDAOSJZ2Q72G5M32MWB + List-Post: + List-Subscribe: + , + + Archived-At: + http://lists.example.com/archives/4CMWUN6BHVCMHMDAOSJZ2Q72G5M32MWB + List-Unsubscribe: + , + + List-Archive: + List-Help: + + First post! + + >>> dump_msgdata(messages[0].msgdata) + _parsemsg : False + listname : xtest@example.com + original_sender : aperson@example.com + origsubj : My first post + recipients : set([]) + stripped_subject: My first post + version : 3 + +There's now one message in the digest mailbox, getting ready to be sent. + + >>> from mailman.testing.helpers import digest_mbox + >>> digest = digest_mbox(mlist) + >>> sum(1 for mboxmsg in digest) + 1 + >>> print list(digest)[0].as_string() + From: aperson@example.com + To: xtest@example.com + Message-ID: + Subject: [Xtest] My first post + X-BeenThere: xtest@example.com + X-Mailman-Version: ... + Precedence: list + List-Id: + X-Message-ID-Hash: 4CMWUN6BHVCMHMDAOSJZ2Q72G5M32MWB + List-Post: + List-Subscribe: + , + + Archived-At: + http://lists.example.com/archives/4CMWUN6BHVCMHMDAOSJZ2Q72G5M32MWB + List-Unsubscribe: + , + + List-Archive: + List-Help: + + First post! + + + + +Clean up the digests +==================== + + >>> digest.clear() + >>> digest.flush() + >>> sum(1 for msg in digest_mbox(mlist)) + 0 diff --git a/src/mailman/app/docs/pipelines.txt b/src/mailman/app/docs/pipelines.txt deleted file mode 100644 index cf848f1d9..000000000 --- a/src/mailman/app/docs/pipelines.txt +++ /dev/null @@ -1,193 +0,0 @@ -========= -Pipelines -========= - -This runner's purpose in life is to process messages that have been accepted -for posting, applying any modifications and also sending copies of the message -to the archives, digests, nntp, and outgoing queues. Pipelines are named and -consist of a sequence of handlers, each of which is applied in turn. Unlike -rules and chains, there is no way to stop a pipeline from processing the -message once it's started. - - >>> mlist = create_list('xtest@example.com') - >>> print mlist.pipeline - built-in - >>> from mailman.core.pipelines import process - - -Processing a message -==================== - -Messages hit the pipeline after they've been accepted for posting. - - >>> msg = message_from_string("""\ - ... From: aperson@example.com - ... To: xtest@example.com - ... Subject: My first post - ... Message-ID: - ... - ... First post! - ... """) - >>> msgdata = {} - >>> process(mlist, msg, msgdata, mlist.pipeline) - -The message has been modified with additional headers, footer decorations, -etc. - - >>> print msg.as_string() - From: aperson@example.com - To: xtest@example.com - Message-ID: - Subject: [Xtest] My first post - X-BeenThere: xtest@example.com - X-Mailman-Version: ... - Precedence: list - List-Id: - X-Message-ID-Hash: 4CMWUN6BHVCMHMDAOSJZ2Q72G5M32MWB - List-Post: - List-Subscribe: - , - - Archived-At: - http://lists.example.com/archives/4CMWUN6BHVCMHMDAOSJZ2Q72G5M32MWB - List-Unsubscribe: - , - - List-Archive: - List-Help: - - First post! - - -And the message metadata has information about recipients and other stuff. -However there are currently no recipients for this message. - - >>> dump_msgdata(msgdata) - original_sender : aperson@example.com - origsubj : My first post - recipients : set([]) - stripped_subject: My first post - -And the message is now sitting in various other processing queues. - - >>> from mailman.testing.helpers import get_queue_messages - >>> messages = get_queue_messages('archive') - >>> len(messages) - 1 - >>> print messages[0].msg.as_string() - From: aperson@example.com - To: xtest@example.com - Message-ID: - Subject: [Xtest] My first post - X-BeenThere: xtest@example.com - X-Mailman-Version: ... - Precedence: list - List-Id: - X-Message-ID-Hash: 4CMWUN6BHVCMHMDAOSJZ2Q72G5M32MWB - List-Post: - List-Subscribe: - , - - Archived-At: - http://lists.example.com/archives/4CMWUN6BHVCMHMDAOSJZ2Q72G5M32MWB - List-Unsubscribe: - , - - List-Archive: - List-Help: - - First post! - - >>> dump_msgdata(messages[0].msgdata) - _parsemsg : False - original_sender : aperson@example.com - origsubj : My first post - recipients : set([]) - stripped_subject: My first post - version : 3 - -This mailing list is not linked to an NNTP newsgroup, so there's nothing in -the outgoing nntp queue. - - >>> messages = get_queue_messages('news') - >>> len(messages) - 0 - -This is the message that will actually get delivered to end recipients. - - >>> messages = get_queue_messages('out') - >>> len(messages) - 1 - >>> print messages[0].msg.as_string() - From: aperson@example.com - To: xtest@example.com - Message-ID: - Subject: [Xtest] My first post - X-BeenThere: xtest@example.com - X-Mailman-Version: ... - Precedence: list - List-Id: - X-Message-ID-Hash: 4CMWUN6BHVCMHMDAOSJZ2Q72G5M32MWB - List-Post: - List-Subscribe: - , - - Archived-At: - http://lists.example.com/archives/4CMWUN6BHVCMHMDAOSJZ2Q72G5M32MWB - List-Unsubscribe: - , - - List-Archive: - List-Help: - - First post! - - >>> dump_msgdata(messages[0].msgdata) - _parsemsg : False - listname : xtest@example.com - original_sender : aperson@example.com - origsubj : My first post - recipients : set([]) - stripped_subject: My first post - version : 3 - -There's now one message in the digest mailbox, getting ready to be sent. - - >>> from mailman.testing.helpers import digest_mbox - >>> digest = digest_mbox(mlist) - >>> sum(1 for mboxmsg in digest) - 1 - >>> print list(digest)[0].as_string() - From: aperson@example.com - To: xtest@example.com - Message-ID: - Subject: [Xtest] My first post - X-BeenThere: xtest@example.com - X-Mailman-Version: ... - Precedence: list - List-Id: - X-Message-ID-Hash: 4CMWUN6BHVCMHMDAOSJZ2Q72G5M32MWB - List-Post: - List-Subscribe: - , - - Archived-At: - http://lists.example.com/archives/4CMWUN6BHVCMHMDAOSJZ2Q72G5M32MWB - List-Unsubscribe: - , - - List-Archive: - List-Help: - - First post! - - - - -Clean up the digests -==================== - - >>> digest.clear() - >>> digest.flush() - >>> sum(1 for msg in digest_mbox(mlist)) - 0 diff --git a/src/mailman/app/docs/styles.rst b/src/mailman/app/docs/styles.rst new file mode 100644 index 000000000..63ec999bf --- /dev/null +++ b/src/mailman/app/docs/styles.rst @@ -0,0 +1,162 @@ +=========== +List styles +=========== + +List styles are a way to name and apply a canned collection of attribute +settings. Every style has a name, which must be unique within the context of +a specific style manager. There is usually only one global style manager. + +Styles also have a priority, which allows you to specify the order in which +multiple styles will be applied. A style has a `match` function which is used +to determine whether the style should be applied to a particular mailing list +or not. And finally, application of a style to a mailing list can really +modify the mailing list any way it wants. + +Let's start with a vanilla mailing list and a default style manager. +:: + + >>> from mailman.interfaces.listmanager import IListManager + >>> from zope.component import getUtility + >>> mlist = getUtility(IListManager).create('_xtest@example.com') + + >>> from mailman.styles.manager import StyleManager + >>> style_manager = StyleManager() + >>> style_manager.populate() + >>> sorted(style.name for style in style_manager.styles) + ['default'] + + +The default style +================= + +There is a default style which implements the legacy application of list +defaults from previous versions of Mailman. This style only matching a +mailing list when no other styles match, and it has the lowest priority. The +low priority means that it is matched last and if it matches, it is applied +last. + + >>> default_style = style_manager.get('default') + >>> default_style.name + 'default' + >>> default_style.priority + 0 + >>> sorted(style.name for style in style_manager.styles) + ['default'] + +Given a mailing list, you can ask the style manager to find all the styles +that match the list. The registered styles will be sorted by decreasing +priority and each style's ``match()`` method will be called in turn. The +sorted list of matching styles will be returned -- but not applied -- by the +style manager's ``lookup()`` method. + + >>> [style.name for style in style_manager.lookup(mlist)] + ['default'] + + +Registering styles +================== + +New styles must implement the ``IStyle`` interface. + + >>> from zope.interface import implements + >>> from mailman.interfaces.styles import IStyle + >>> class TestStyle(object): + ... implements(IStyle) + ... name = 'test' + ... priority = 10 + ... def apply(self, mailing_list): + ... # Just does something very simple. + ... mailing_list.msg_footer = 'test footer' + ... def match(self, mailing_list, styles): + ... # Applies to any test list + ... if 'test' in mailing_list.fqdn_listname: + ... styles.append(self) + +You can register a new style with the style manager. + + >>> style_manager.register(TestStyle()) + +And now if you lookup matching styles, you should find only the new test +style. This is because the default style only gets applied when no other +styles match the mailing list. + + >>> sorted(style.name for style in style_manager.lookup(mlist)) + [u'test'] + >>> for style in style_manager.lookup(mlist): + ... style.apply(mlist) + >>> print mlist.msg_footer + test footer + + +Style priority +============== + +When multiple styles match a particular mailing list, they are applied in +descending order of priority. In other words, a priority zero style would be +applied last. +:: + + >>> class AnotherTestStyle(TestStyle): + ... name = 'another' + ... priority = 5 + ... # Use the base class's match() method. + ... def apply(self, mailing_list): + ... mailing_list.msg_footer = 'another footer' + + >>> mlist.msg_footer = '' + >>> mlist.msg_footer + u'' + >>> style_manager.register(AnotherTestStyle()) + >>> for style in style_manager.lookup(mlist): + ... style.apply(mlist) + >>> print mlist.msg_footer + another footer + +You can change the priority of a style, and if you reapply the styles, they +will take effect in the new priority order. + + >>> style_1 = style_manager.get('test') + >>> style_1.priority = 5 + >>> style_2 = style_manager.get('another') + >>> style_2.priority = 10 + >>> for style in style_manager.lookup(mlist): + ... style.apply(mlist) + >>> print mlist.msg_footer + test footer + + +Unregistering styles +==================== + +You can unregister a style, making it unavailable in the future. + + >>> style_manager.unregister(style_2) + >>> sorted(style.name for style in style_manager.lookup(mlist)) + [u'test'] + + +Corner cases +============ + +If you register a style with the same name as an already registered style, you +get an exception. + + >>> style_manager.register(TestStyle()) + Traceback (most recent call last): + ... + DuplicateStyleError: test + +If you try to register an object that isn't a style, you get an exception. + + >>> style_manager.register(object()) + Traceback (most recent call last): + ... + DoesNotImplement: An object does not implement interface + + +If you try to unregister a style that isn't registered, you get an exception. + + >>> style_manager.unregister(style_2) + Traceback (most recent call last): + ... + KeyError: u'another' diff --git a/src/mailman/app/docs/styles.txt b/src/mailman/app/docs/styles.txt deleted file mode 100644 index 63ec999bf..000000000 --- a/src/mailman/app/docs/styles.txt +++ /dev/null @@ -1,162 +0,0 @@ -=========== -List styles -=========== - -List styles are a way to name and apply a canned collection of attribute -settings. Every style has a name, which must be unique within the context of -a specific style manager. There is usually only one global style manager. - -Styles also have a priority, which allows you to specify the order in which -multiple styles will be applied. A style has a `match` function which is used -to determine whether the style should be applied to a particular mailing list -or not. And finally, application of a style to a mailing list can really -modify the mailing list any way it wants. - -Let's start with a vanilla mailing list and a default style manager. -:: - - >>> from mailman.interfaces.listmanager import IListManager - >>> from zope.component import getUtility - >>> mlist = getUtility(IListManager).create('_xtest@example.com') - - >>> from mailman.styles.manager import StyleManager - >>> style_manager = StyleManager() - >>> style_manager.populate() - >>> sorted(style.name for style in style_manager.styles) - ['default'] - - -The default style -================= - -There is a default style which implements the legacy application of list -defaults from previous versions of Mailman. This style only matching a -mailing list when no other styles match, and it has the lowest priority. The -low priority means that it is matched last and if it matches, it is applied -last. - - >>> default_style = style_manager.get('default') - >>> default_style.name - 'default' - >>> default_style.priority - 0 - >>> sorted(style.name for style in style_manager.styles) - ['default'] - -Given a mailing list, you can ask the style manager to find all the styles -that match the list. The registered styles will be sorted by decreasing -priority and each style's ``match()`` method will be called in turn. The -sorted list of matching styles will be returned -- but not applied -- by the -style manager's ``lookup()`` method. - - >>> [style.name for style in style_manager.lookup(mlist)] - ['default'] - - -Registering styles -================== - -New styles must implement the ``IStyle`` interface. - - >>> from zope.interface import implements - >>> from mailman.interfaces.styles import IStyle - >>> class TestStyle(object): - ... implements(IStyle) - ... name = 'test' - ... priority = 10 - ... def apply(self, mailing_list): - ... # Just does something very simple. - ... mailing_list.msg_footer = 'test footer' - ... def match(self, mailing_list, styles): - ... # Applies to any test list - ... if 'test' in mailing_list.fqdn_listname: - ... styles.append(self) - -You can register a new style with the style manager. - - >>> style_manager.register(TestStyle()) - -And now if you lookup matching styles, you should find only the new test -style. This is because the default style only gets applied when no other -styles match the mailing list. - - >>> sorted(style.name for style in style_manager.lookup(mlist)) - [u'test'] - >>> for style in style_manager.lookup(mlist): - ... style.apply(mlist) - >>> print mlist.msg_footer - test footer - - -Style priority -============== - -When multiple styles match a particular mailing list, they are applied in -descending order of priority. In other words, a priority zero style would be -applied last. -:: - - >>> class AnotherTestStyle(TestStyle): - ... name = 'another' - ... priority = 5 - ... # Use the base class's match() method. - ... def apply(self, mailing_list): - ... mailing_list.msg_footer = 'another footer' - - >>> mlist.msg_footer = '' - >>> mlist.msg_footer - u'' - >>> style_manager.register(AnotherTestStyle()) - >>> for style in style_manager.lookup(mlist): - ... style.apply(mlist) - >>> print mlist.msg_footer - another footer - -You can change the priority of a style, and if you reapply the styles, they -will take effect in the new priority order. - - >>> style_1 = style_manager.get('test') - >>> style_1.priority = 5 - >>> style_2 = style_manager.get('another') - >>> style_2.priority = 10 - >>> for style in style_manager.lookup(mlist): - ... style.apply(mlist) - >>> print mlist.msg_footer - test footer - - -Unregistering styles -==================== - -You can unregister a style, making it unavailable in the future. - - >>> style_manager.unregister(style_2) - >>> sorted(style.name for style in style_manager.lookup(mlist)) - [u'test'] - - -Corner cases -============ - -If you register a style with the same name as an already registered style, you -get an exception. - - >>> style_manager.register(TestStyle()) - Traceback (most recent call last): - ... - DuplicateStyleError: test - -If you try to register an object that isn't a style, you get an exception. - - >>> style_manager.register(object()) - Traceback (most recent call last): - ... - DoesNotImplement: An object does not implement interface - - -If you try to unregister a style that isn't registered, you get an exception. - - >>> style_manager.unregister(style_2) - Traceback (most recent call last): - ... - KeyError: u'another' diff --git a/src/mailman/app/docs/system.rst b/src/mailman/app/docs/system.rst new file mode 100644 index 000000000..844db9ee6 --- /dev/null +++ b/src/mailman/app/docs/system.rst @@ -0,0 +1,29 @@ +=============== +System versions +=============== + +Mailman system information is available through the ``system`` object, which +implements the ``ISystem`` interface. +:: + + >>> from mailman.interfaces.system import ISystem + >>> from mailman.core.system import system + >>> from zope.interface.verify import verifyObject + + >>> verifyObject(ISystem, system) + True + +The Mailman version is also available via the ``system`` object. + + >>> print system.mailman_version + GNU Mailman ... + +The Python version running underneath is also available via the ``system`` +object. +:: + + # The entire python_version string is variable, so this is the best test + # we can do. + >>> import sys + >>> system.python_version == sys.version + True diff --git a/src/mailman/app/docs/system.txt b/src/mailman/app/docs/system.txt deleted file mode 100644 index 844db9ee6..000000000 --- a/src/mailman/app/docs/system.txt +++ /dev/null @@ -1,29 +0,0 @@ -=============== -System versions -=============== - -Mailman system information is available through the ``system`` object, which -implements the ``ISystem`` interface. -:: - - >>> from mailman.interfaces.system import ISystem - >>> from mailman.core.system import system - >>> from zope.interface.verify import verifyObject - - >>> verifyObject(ISystem, system) - True - -The Mailman version is also available via the ``system`` object. - - >>> print system.mailman_version - GNU Mailman ... - -The Python version running underneath is also available via the ``system`` -object. -:: - - # The entire python_version string is variable, so this is the best test - # we can do. - >>> import sys - >>> system.python_version == sys.version - True diff --git a/src/mailman/archiving/docs/common.rst b/src/mailman/archiving/docs/common.rst new file mode 100644 index 000000000..330c8e307 --- /dev/null +++ b/src/mailman/archiving/docs/common.rst @@ -0,0 +1,180 @@ +========= +Archivers +========= + +Mailman supports pluggable archivers, and it comes with several default +archivers. + + >>> mlist = create_list('test@example.com') + >>> msg = message_from_string("""\ + ... From: aperson@example.org + ... To: test@example.com + ... Subject: An archived message + ... Message-ID: <12345> + ... + ... Here is an archived message. + ... """) + +Archivers support an interface which provides the RFC 2369 ``List-Archive:`` +header, and one that provides a *permalink* to the specific message object in +the archive. This latter is appropriate for the message footer or for the RFC +5064 ``Archived-At:`` header. + +Pipermail does not support a permalink, so that interface returns ``None``. +Mailman defines a draft spec for how list servers and archivers can +interoperate. + + >>> archivers = {} + >>> from operator import attrgetter + >>> for archiver in sorted(config.archivers, key=attrgetter('name')): + ... print archiver.name + ... print ' ', archiver.list_url(mlist) + ... print ' ', archiver.permalink(mlist, msg) + ... archivers[archiver.name] = archiver + mail-archive + http://go.mail-archive.dev/test%40example.com + http://go.mail-archive.dev/ZaXPPxRMM9_hFZL4vTRlQlBx8pc= + mhonarc + http://lists.example.com/.../test@example.com + http://lists.example.com/.../RSZCG7IGPHFIRW3EMTVMMDNJMNCVCOLE + pipermail + http://www.example.com/pipermail/test@example.com + None + prototype + http://lists.example.com + http://lists.example.com/RSZCG7IGPHFIRW3EMTVMMDNJMNCVCOLE + + +Sending the message to the archiver +=================================== + +The archiver is also able to archive the message. +:: + + >>> archivers['pipermail'].archive_message(mlist, msg) + + >>> import os + >>> from mailman.interfaces.archiver import IPipermailMailingList + >>> pckpath = os.path.join( + ... IPipermailMailingList(mlist).archive_dir(), + ... 'pipermail.pck') + >>> os.path.exists(pckpath) + True + +Note however that the prototype archiver can't archive messages. + + >>> archivers['prototype'].archive_message(mlist, msg) + Traceback (most recent call last): + ... + NotImplementedError + + +The Mail-Archive.com +==================== + +`The Mail Archive`_ is a public archiver that can be used to archive message +for free. Mailman comes with a plugin for this archiver; by enabling it +messages to public lists will get sent there automatically. + + >>> archiver = archivers['mail-archive'] + >>> print archiver.list_url(mlist) + http://go.mail-archive.dev/test%40example.com + >>> print archiver.permalink(mlist, msg) + http://go.mail-archive.dev/ZaXPPxRMM9_hFZL4vTRlQlBx8pc= + +To archive the message, the archiver actually mails the message to a special +address at The Mail Archive. The message gets no header or footer decoration. +:: + + >>> archiver.archive_message(mlist, msg) + + >>> from mailman.runners.outgoing import OutgoingRunner + >>> from mailman.testing.helpers import make_testable_runner + >>> outgoing = make_testable_runner(OutgoingRunner, 'out') + >>> outgoing.run() + + >>> from operator import itemgetter + >>> messages = list(smtpd.messages) + >>> len(messages) + 1 + + >>> print messages[0].as_string() + From: aperson@example.org + To: test@example.com + Subject: An archived message + Message-ID: <12345> + X-Message-ID-Hash: ZaXPPxRMM9_hFZL4vTRlQlBx8pc= + X-Peer: 127.0.0.1:... + X-MailFrom: test-bounces@example.com + X-RcptTo: archive@mail-archive.dev + + Here is an archived message. + + >>> smtpd.clear() + +However, if the mailing list is not public, the message will never be archived +at this service. + + >>> mlist.archive_private = True + >>> print archiver.list_url(mlist) + None + >>> print archiver.permalink(mlist, msg) + None + >>> archiver.archive_message(mlist, msg) + >>> list(smtpd.messages) + [] + +Additionally, this archiver can handle malformed ``Message-IDs``. +:: + + >>> mlist.archive_private = False + >>> del msg['message-id'] + >>> msg['Message-ID'] = '12345>' + >>> print archiver.permalink(mlist, msg) + http://go.mail-archive.dev/bXvG32YzcDEIVDaDLaUSVQekfo8= + + >>> del msg['message-id'] + >>> msg['Message-ID'] = '<12345' + >>> print archiver.permalink(mlist, msg) + http://go.mail-archive.dev/9rockPrT1Mm-jOsLWS6_hseR_OY= + + >>> del msg['message-id'] + >>> msg['Message-ID'] = '12345' + >>> print archiver.permalink(mlist, msg) + http://go.mail-archive.dev/ZaXPPxRMM9_hFZL4vTRlQlBx8pc= + + >>> del msg['message-id'] + >>> msg['Message-ID'] = ' 12345 ' + >>> print archiver.permalink(mlist, msg) + http://go.mail-archive.dev/ZaXPPxRMM9_hFZL4vTRlQlBx8pc= + + +MHonArc +======= + +A MHonArc_ archiver is also available. + + >>> archiver = archivers['mhonarc'] + >>> print archiver.name + mhonarc + +Messages sent to a local MHonArc instance are added to its archive via a +subprocess call. + + >>> archiver.archive_message(mlist, msg) + >>> archive_log = open(os.path.join(config.LOG_DIR, 'archiver')) + >>> try: + ... contents = archive_log.read() + ... finally: + ... archive_log.close() + >>> print 'LOG:', contents + LOG: ... /usr/bin/mhonarc -add + -dbfile /.../private/test@example.com.mbox/mhonarc.db + -outdir /.../mhonarc/test@example.com + -stderr /.../logs/mhonarc + -stdout /.../logs/mhonarc + -spammode -umask 022 + ... + +.. _`The Mail Archive`: http://www.mail-archive.com +.. _MHonArc: http://www.mhonarc.org diff --git a/src/mailman/archiving/docs/common.txt b/src/mailman/archiving/docs/common.txt deleted file mode 100644 index 330c8e307..000000000 --- a/src/mailman/archiving/docs/common.txt +++ /dev/null @@ -1,180 +0,0 @@ -========= -Archivers -========= - -Mailman supports pluggable archivers, and it comes with several default -archivers. - - >>> mlist = create_list('test@example.com') - >>> msg = message_from_string("""\ - ... From: aperson@example.org - ... To: test@example.com - ... Subject: An archived message - ... Message-ID: <12345> - ... - ... Here is an archived message. - ... """) - -Archivers support an interface which provides the RFC 2369 ``List-Archive:`` -header, and one that provides a *permalink* to the specific message object in -the archive. This latter is appropriate for the message footer or for the RFC -5064 ``Archived-At:`` header. - -Pipermail does not support a permalink, so that interface returns ``None``. -Mailman defines a draft spec for how list servers and archivers can -interoperate. - - >>> archivers = {} - >>> from operator import attrgetter - >>> for archiver in sorted(config.archivers, key=attrgetter('name')): - ... print archiver.name - ... print ' ', archiver.list_url(mlist) - ... print ' ', archiver.permalink(mlist, msg) - ... archivers[archiver.name] = archiver - mail-archive - http://go.mail-archive.dev/test%40example.com - http://go.mail-archive.dev/ZaXPPxRMM9_hFZL4vTRlQlBx8pc= - mhonarc - http://lists.example.com/.../test@example.com - http://lists.example.com/.../RSZCG7IGPHFIRW3EMTVMMDNJMNCVCOLE - pipermail - http://www.example.com/pipermail/test@example.com - None - prototype - http://lists.example.com - http://lists.example.com/RSZCG7IGPHFIRW3EMTVMMDNJMNCVCOLE - - -Sending the message to the archiver -=================================== - -The archiver is also able to archive the message. -:: - - >>> archivers['pipermail'].archive_message(mlist, msg) - - >>> import os - >>> from mailman.interfaces.archiver import IPipermailMailingList - >>> pckpath = os.path.join( - ... IPipermailMailingList(mlist).archive_dir(), - ... 'pipermail.pck') - >>> os.path.exists(pckpath) - True - -Note however that the prototype archiver can't archive messages. - - >>> archivers['prototype'].archive_message(mlist, msg) - Traceback (most recent call last): - ... - NotImplementedError - - -The Mail-Archive.com -==================== - -`The Mail Archive`_ is a public archiver that can be used to archive message -for free. Mailman comes with a plugin for this archiver; by enabling it -messages to public lists will get sent there automatically. - - >>> archiver = archivers['mail-archive'] - >>> print archiver.list_url(mlist) - http://go.mail-archive.dev/test%40example.com - >>> print archiver.permalink(mlist, msg) - http://go.mail-archive.dev/ZaXPPxRMM9_hFZL4vTRlQlBx8pc= - -To archive the message, the archiver actually mails the message to a special -address at The Mail Archive. The message gets no header or footer decoration. -:: - - >>> archiver.archive_message(mlist, msg) - - >>> from mailman.runners.outgoing import OutgoingRunner - >>> from mailman.testing.helpers import make_testable_runner - >>> outgoing = make_testable_runner(OutgoingRunner, 'out') - >>> outgoing.run() - - >>> from operator import itemgetter - >>> messages = list(smtpd.messages) - >>> len(messages) - 1 - - >>> print messages[0].as_string() - From: aperson@example.org - To: test@example.com - Subject: An archived message - Message-ID: <12345> - X-Message-ID-Hash: ZaXPPxRMM9_hFZL4vTRlQlBx8pc= - X-Peer: 127.0.0.1:... - X-MailFrom: test-bounces@example.com - X-RcptTo: archive@mail-archive.dev - - Here is an archived message. - - >>> smtpd.clear() - -However, if the mailing list is not public, the message will never be archived -at this service. - - >>> mlist.archive_private = True - >>> print archiver.list_url(mlist) - None - >>> print archiver.permalink(mlist, msg) - None - >>> archiver.archive_message(mlist, msg) - >>> list(smtpd.messages) - [] - -Additionally, this archiver can handle malformed ``Message-IDs``. -:: - - >>> mlist.archive_private = False - >>> del msg['message-id'] - >>> msg['Message-ID'] = '12345>' - >>> print archiver.permalink(mlist, msg) - http://go.mail-archive.dev/bXvG32YzcDEIVDaDLaUSVQekfo8= - - >>> del msg['message-id'] - >>> msg['Message-ID'] = '<12345' - >>> print archiver.permalink(mlist, msg) - http://go.mail-archive.dev/9rockPrT1Mm-jOsLWS6_hseR_OY= - - >>> del msg['message-id'] - >>> msg['Message-ID'] = '12345' - >>> print archiver.permalink(mlist, msg) - http://go.mail-archive.dev/ZaXPPxRMM9_hFZL4vTRlQlBx8pc= - - >>> del msg['message-id'] - >>> msg['Message-ID'] = ' 12345 ' - >>> print archiver.permalink(mlist, msg) - http://go.mail-archive.dev/ZaXPPxRMM9_hFZL4vTRlQlBx8pc= - - -MHonArc -======= - -A MHonArc_ archiver is also available. - - >>> archiver = archivers['mhonarc'] - >>> print archiver.name - mhonarc - -Messages sent to a local MHonArc instance are added to its archive via a -subprocess call. - - >>> archiver.archive_message(mlist, msg) - >>> archive_log = open(os.path.join(config.LOG_DIR, 'archiver')) - >>> try: - ... contents = archive_log.read() - ... finally: - ... archive_log.close() - >>> print 'LOG:', contents - LOG: ... /usr/bin/mhonarc -add - -dbfile /.../private/test@example.com.mbox/mhonarc.db - -outdir /.../mhonarc/test@example.com - -stderr /.../logs/mhonarc - -stdout /.../logs/mhonarc - -spammode -umask 022 - ... - -.. _`The Mail Archive`: http://www.mail-archive.com -.. _MHonArc: http://www.mhonarc.org diff --git a/src/mailman/commands/docs/echo.rst b/src/mailman/commands/docs/echo.rst new file mode 100644 index 000000000..ced483ea8 --- /dev/null +++ b/src/mailman/commands/docs/echo.rst @@ -0,0 +1,30 @@ +The 'echo' command +================== + +The mail command 'echo' simply replies with the original command and arguments +to the sender. + + >>> command = config.commands['echo'] + >>> command.name + 'echo' + >>> command.argument_description + '[args]' + >>> print command.description + Echo an acknowledgement. Arguments are return unchanged. + +The original message is ignored, but the results receive the echoed command. +:: + + >>> mlist = create_list('test@example.com') + + >>> from mailman.runners.command import Results + >>> results = Results() + + >>> from mailman.email.message import Message + >>> print command.process(mlist, Message(), {}, ('foo', 'bar'), results) + ContinueProcessing.yes + >>> print unicode(results) + The results of your email command are provided below. + + echo foo bar + diff --git a/src/mailman/commands/docs/echo.txt b/src/mailman/commands/docs/echo.txt deleted file mode 100644 index ced483ea8..000000000 --- a/src/mailman/commands/docs/echo.txt +++ /dev/null @@ -1,30 +0,0 @@ -The 'echo' command -================== - -The mail command 'echo' simply replies with the original command and arguments -to the sender. - - >>> command = config.commands['echo'] - >>> command.name - 'echo' - >>> command.argument_description - '[args]' - >>> print command.description - Echo an acknowledgement. Arguments are return unchanged. - -The original message is ignored, but the results receive the echoed command. -:: - - >>> mlist = create_list('test@example.com') - - >>> from mailman.runners.command import Results - >>> results = Results() - - >>> from mailman.email.message import Message - >>> print command.process(mlist, Message(), {}, ('foo', 'bar'), results) - ContinueProcessing.yes - >>> print unicode(results) - The results of your email command are provided below. - - echo foo bar - diff --git a/src/mailman/commands/docs/end.rst b/src/mailman/commands/docs/end.rst new file mode 100644 index 000000000..a11bea095 --- /dev/null +++ b/src/mailman/commands/docs/end.rst @@ -0,0 +1,36 @@ +The 'end' command +================= + +The mail command processor recognized an 'end' command which tells it to stop +processing email messages. + + >>> command = config.commands['end'] + >>> command.name + 'end' + >>> print command.description + Stop processing commands. + +The 'end' command takes no arguments. + + >>> command.argument_description + '' + +The command itself is fairly simple; it just stops command processing, and the +message isn't even looked at. + + >>> mlist = create_list('test@example.com') + >>> from mailman.email.message import Message + >>> print command.process(mlist, Message(), {}, (), None) + ContinueProcessing.no + +The 'stop' command is a synonym for 'end'. + + >>> command = config.commands['stop'] + >>> print command.name + stop + >>> print command.description + Stop processing commands. + >>> command.argument_description + '' + >>> print command.process(mlist, Message(), {}, (), None) + ContinueProcessing.no diff --git a/src/mailman/commands/docs/end.txt b/src/mailman/commands/docs/end.txt deleted file mode 100644 index a11bea095..000000000 --- a/src/mailman/commands/docs/end.txt +++ /dev/null @@ -1,36 +0,0 @@ -The 'end' command -================= - -The mail command processor recognized an 'end' command which tells it to stop -processing email messages. - - >>> command = config.commands['end'] - >>> command.name - 'end' - >>> print command.description - Stop processing commands. - -The 'end' command takes no arguments. - - >>> command.argument_description - '' - -The command itself is fairly simple; it just stops command processing, and the -message isn't even looked at. - - >>> mlist = create_list('test@example.com') - >>> from mailman.email.message import Message - >>> print command.process(mlist, Message(), {}, (), None) - ContinueProcessing.no - -The 'stop' command is a synonym for 'end'. - - >>> command = config.commands['stop'] - >>> print command.name - stop - >>> print command.description - Stop processing commands. - >>> command.argument_description - '' - >>> print command.process(mlist, Message(), {}, (), None) - ContinueProcessing.no diff --git a/src/mailman/commands/docs/import.rst b/src/mailman/commands/docs/import.rst new file mode 100644 index 000000000..34521026d --- /dev/null +++ b/src/mailman/commands/docs/import.rst @@ -0,0 +1,56 @@ +=================== +Importing list data +=================== + +If you have the config.pck file for a version 2.1 mailing list, you can import +that into an existing mailing list in Mailman 3.0. +:: + + >>> from mailman.commands.cli_import import Import21 + >>> command = Import21() + + >>> class FakeArgs: + ... listname = None + ... pickle_file = None + + >>> class FakeParser: + ... def error(self, message): + ... print message + >>> command.parser = FakeParser() + +You must specify the mailing list you are importing into, and it must exist. +:: + + >>> command.process(FakeArgs) + List name is required + + >>> FakeArgs.listname = ['import@example.com'] + >>> command.process(FakeArgs) + No such list: import@example.com + +When the mailing list exists, you must specify a real pickle file to import +from. +:: + + >>> mlist = create_list('import@example.com') + >>> command.process(FakeArgs) + config.pck file is required + + >>> FakeArgs.pickle_file = [__file__] + >>> command.process(FakeArgs) + Not a Mailman 2.1 configuration file: .../import.txt + +Now we can import the test pickle file. As a simple illustration of the +import, the mailing list's 'real name' has changed. +:: + + >>> from pkg_resources import resource_filename + >>> FakeArgs.pickle_file = [ + ... resource_filename('mailman.testing', 'config.pck')] + + >>> print mlist.real_name + Import + + >>> command.process(FakeArgs) + >>> print mlist.real_name + Test diff --git a/src/mailman/commands/docs/import.txt b/src/mailman/commands/docs/import.txt deleted file mode 100644 index 34521026d..000000000 --- a/src/mailman/commands/docs/import.txt +++ /dev/null @@ -1,56 +0,0 @@ -=================== -Importing list data -=================== - -If you have the config.pck file for a version 2.1 mailing list, you can import -that into an existing mailing list in Mailman 3.0. -:: - - >>> from mailman.commands.cli_import import Import21 - >>> command = Import21() - - >>> class FakeArgs: - ... listname = None - ... pickle_file = None - - >>> class FakeParser: - ... def error(self, message): - ... print message - >>> command.parser = FakeParser() - -You must specify the mailing list you are importing into, and it must exist. -:: - - >>> command.process(FakeArgs) - List name is required - - >>> FakeArgs.listname = ['import@example.com'] - >>> command.process(FakeArgs) - No such list: import@example.com - -When the mailing list exists, you must specify a real pickle file to import -from. -:: - - >>> mlist = create_list('import@example.com') - >>> command.process(FakeArgs) - config.pck file is required - - >>> FakeArgs.pickle_file = [__file__] - >>> command.process(FakeArgs) - Not a Mailman 2.1 configuration file: .../import.txt - -Now we can import the test pickle file. As a simple illustration of the -import, the mailing list's 'real name' has changed. -:: - - >>> from pkg_resources import resource_filename - >>> FakeArgs.pickle_file = [ - ... resource_filename('mailman.testing', 'config.pck')] - - >>> print mlist.real_name - Import - - >>> command.process(FakeArgs) - >>> print mlist.real_name - Test diff --git a/src/mailman/commands/docs/info.rst b/src/mailman/commands/docs/info.rst new file mode 100644 index 000000000..83b3fe179 --- /dev/null +++ b/src/mailman/commands/docs/info.rst @@ -0,0 +1,82 @@ +=================== +Getting information +=================== + +You can get information about Mailman's environment by using the command line +script ``mailman info``. By default, the info is printed to standard output. +:: + + >>> from mailman.commands.cli_info import Info + >>> command = Info() + + >>> class FakeArgs: + ... output = None + ... verbose = None + >>> args = FakeArgs() + + >>> command.process(args) + GNU Mailman 3... + Python ... + ... + config file: .../test.cfg + db url: sqlite:.../mailman.db + REST root url: http://localhost:9001/3.0/ + REST credentials: restadmin:restpass + +By passing in the ``-o/--output`` option, you can print the info to a file. + + >>> from mailman.config import config + >>> import os + >>> output_path = os.path.join(config.VAR_DIR, 'output.txt') + >>> args.output = output_path + >>> command.process(args) + >>> with open(output_path) as fp: + ... print fp.read() + GNU Mailman 3... + Python ... + ... + config file: .../test.cfg + db url: sqlite:.../mailman.db + REST root url: http://localhost:9001/3.0/ + REST credentials: restadmin:restpass + +You can also get more verbose information, which contains a list of the file +system paths that Mailman is using. + + >>> args.output = None + >>> args.verbose = True + >>> config.create_paths = False + >>> config.push('fhs', """ + ... [mailman] + ... layout: fhs + ... """) + >>> config.create_paths = True + +The File System Hierarchy layout is the same every by definition. + + >>> command.process(args) + GNU Mailman 3... + Python ... + ... + File system paths: + BIN_DIR = /sbin + DATA_DIR = /var/lib/mailman/data + ETC_DIR = /etc + EXT_DIR = /etc/mailman.d + LIST_DATA_DIR = /var/lib/mailman/lists + LOCK_DIR = /var/lock/mailman + LOCK_FILE = /var/lock/mailman/master.lck + LOG_DIR = /var/log/mailman + MESSAGES_DIR = /var/lib/mailman/messages + PID_FILE = /var/run/mailman/master.pid + PRIVATE_ARCHIVE_FILE_DIR = /var/lib/mailman/archives/private + PUBLIC_ARCHIVE_FILE_DIR = /var/lib/mailman/archives/public + QUEUE_DIR = /var/spool/mailman + TEMPLATE_DIR = .../mailman/templates + VAR_DIR = /var/lib/mailman + + +Clean up +======== + + >>> config.pop('fhs') diff --git a/src/mailman/commands/docs/info.txt b/src/mailman/commands/docs/info.txt deleted file mode 100644 index 83b3fe179..000000000 --- a/src/mailman/commands/docs/info.txt +++ /dev/null @@ -1,82 +0,0 @@ -=================== -Getting information -=================== - -You can get information about Mailman's environment by using the command line -script ``mailman info``. By default, the info is printed to standard output. -:: - - >>> from mailman.commands.cli_info import Info - >>> command = Info() - - >>> class FakeArgs: - ... output = None - ... verbose = None - >>> args = FakeArgs() - - >>> command.process(args) - GNU Mailman 3... - Python ... - ... - config file: .../test.cfg - db url: sqlite:.../mailman.db - REST root url: http://localhost:9001/3.0/ - REST credentials: restadmin:restpass - -By passing in the ``-o/--output`` option, you can print the info to a file. - - >>> from mailman.config import config - >>> import os - >>> output_path = os.path.join(config.VAR_DIR, 'output.txt') - >>> args.output = output_path - >>> command.process(args) - >>> with open(output_path) as fp: - ... print fp.read() - GNU Mailman 3... - Python ... - ... - config file: .../test.cfg - db url: sqlite:.../mailman.db - REST root url: http://localhost:9001/3.0/ - REST credentials: restadmin:restpass - -You can also get more verbose information, which contains a list of the file -system paths that Mailman is using. - - >>> args.output = None - >>> args.verbose = True - >>> config.create_paths = False - >>> config.push('fhs', """ - ... [mailman] - ... layout: fhs - ... """) - >>> config.create_paths = True - -The File System Hierarchy layout is the same every by definition. - - >>> command.process(args) - GNU Mailman 3... - Python ... - ... - File system paths: - BIN_DIR = /sbin - DATA_DIR = /var/lib/mailman/data - ETC_DIR = /etc - EXT_DIR = /etc/mailman.d - LIST_DATA_DIR = /var/lib/mailman/lists - LOCK_DIR = /var/lock/mailman - LOCK_FILE = /var/lock/mailman/master.lck - LOG_DIR = /var/log/mailman - MESSAGES_DIR = /var/lib/mailman/messages - PID_FILE = /var/run/mailman/master.pid - PRIVATE_ARCHIVE_FILE_DIR = /var/lib/mailman/archives/private - PUBLIC_ARCHIVE_FILE_DIR = /var/lib/mailman/archives/public - QUEUE_DIR = /var/spool/mailman - TEMPLATE_DIR = .../mailman/templates - VAR_DIR = /var/lib/mailman - - -Clean up -======== - - >>> config.pop('fhs') diff --git a/src/mailman/commands/docs/lists.rst b/src/mailman/commands/docs/lists.rst new file mode 100644 index 000000000..036147a23 --- /dev/null +++ b/src/mailman/commands/docs/lists.rst @@ -0,0 +1,141 @@ +========================= +Command line list display +========================= + +A system administrator can display all the mailing lists via the command +line. When there are no mailing lists, a helpful message is displayed. +:: + + >>> class FakeArgs: + ... advertised = False + ... names = False + ... descriptions = False + ... quiet = False + ... domains = None + + >>> from mailman.commands.cli_lists import Lists + >>> command = Lists() + >>> command.process(FakeArgs) + No matching mailing lists found + +When there are a few mailing lists, they are shown in alphabetical order by +their fully qualified list names, with a description. +:: + + >>> from mailman.interfaces.domain import IDomainManager + >>> from zope.component import getUtility + >>> getUtility(IDomainManager).add('example.net') + + + >>> mlist_1 = create_list('list-one@example.com') + >>> mlist_1.description = 'List One' + + >>> mlist_2 = create_list('list-two@example.com') + >>> mlist_2.description = 'List Two' + + >>> mlist_3 = create_list('list-one@example.net') + >>> mlist_3.description = 'List One in Example.Net' + + >>> command.process(FakeArgs) + 3 matching mailing lists found: + list-one@example.com + list-one@example.net + list-two@example.com + + +Names +===== + +You can display the mailing list names with their posting addresses, using the +``--names/-n`` switch. + + >>> FakeArgs.names = True + >>> command.process(FakeArgs) + 3 matching mailing lists found: + list-one@example.com [List-one] + list-one@example.net [List-one] + list-two@example.com [List-two] + + +Descriptions +============ + +You can also display the mailing list descriptions, using the +``--descriptions/-d`` option. + + >>> FakeArgs.descriptions = True + >>> command.process(FakeArgs) + 3 matching mailing lists found: + list-one@example.com [List-one] - List One + list-one@example.net [List-one] - List One in Example.Net + list-two@example.com [List-two] - List Two + +Maybe you want the descriptions but not the names. + + >>> FakeArgs.names = False + >>> command.process(FakeArgs) + 3 matching mailing lists found: + list-one@example.com - List One + list-one@example.net - List One in Example.Net + list-two@example.com - List Two + + +Less verbosity +============== + +There's also a ``--quiet/-q`` switch which reduces the verbosity a bit. + + >>> FakeArgs.quiet = True + >>> FakeArgs.descriptions = False + >>> command.process(FakeArgs) + list-one@example.com + list-one@example.net + list-two@example.com + + +Specific domain +=============== + +You can narrow the search down to a specific domain with the --domain option. +A helpful message is displayed if no matching domains are given. + + >>> FakeArgs.quiet = False + >>> FakeArgs.domains = ['example.org'] + >>> command.process(FakeArgs) + No matching mailing lists found + +But if a matching domain is given, only mailing lists in that domain are +shown. + + >>> FakeArgs.domains = ['example.net'] + >>> command.process(FakeArgs) + 1 matching mailing lists found: + list-one@example.net + +More than one --domain argument can be given; then all mailing lists in +matching domains are shown. + + >>> FakeArgs.domains = ['example.com', 'example.net'] + >>> command.process(FakeArgs) + 3 matching mailing lists found: + list-one@example.com + list-one@example.net + list-two@example.com + + +Advertised lists +================ + +Mailing lists can be 'advertised' meaning their existence is public +knowledge. Non-advertised lists are considered private. Display through the +command line can select on this attribute. +:: + + >>> FakeArgs.domains = [] + >>> FakeArgs.advertised = True + >>> mlist_1.advertised = False + + >>> command.process(FakeArgs) + 2 matching mailing lists found: + list-one@example.net + list-two@example.com diff --git a/src/mailman/commands/docs/lists.txt b/src/mailman/commands/docs/lists.txt deleted file mode 100644 index 036147a23..000000000 --- a/src/mailman/commands/docs/lists.txt +++ /dev/null @@ -1,141 +0,0 @@ -========================= -Command line list display -========================= - -A system administrator can display all the mailing lists via the command -line. When there are no mailing lists, a helpful message is displayed. -:: - - >>> class FakeArgs: - ... advertised = False - ... names = False - ... descriptions = False - ... quiet = False - ... domains = None - - >>> from mailman.commands.cli_lists import Lists - >>> command = Lists() - >>> command.process(FakeArgs) - No matching mailing lists found - -When there are a few mailing lists, they are shown in alphabetical order by -their fully qualified list names, with a description. -:: - - >>> from mailman.interfaces.domain import IDomainManager - >>> from zope.component import getUtility - >>> getUtility(IDomainManager).add('example.net') - - - >>> mlist_1 = create_list('list-one@example.com') - >>> mlist_1.description = 'List One' - - >>> mlist_2 = create_list('list-two@example.com') - >>> mlist_2.description = 'List Two' - - >>> mlist_3 = create_list('list-one@example.net') - >>> mlist_3.description = 'List One in Example.Net' - - >>> command.process(FakeArgs) - 3 matching mailing lists found: - list-one@example.com - list-one@example.net - list-two@example.com - - -Names -===== - -You can display the mailing list names with their posting addresses, using the -``--names/-n`` switch. - - >>> FakeArgs.names = True - >>> command.process(FakeArgs) - 3 matching mailing lists found: - list-one@example.com [List-one] - list-one@example.net [List-one] - list-two@example.com [List-two] - - -Descriptions -============ - -You can also display the mailing list descriptions, using the -``--descriptions/-d`` option. - - >>> FakeArgs.descriptions = True - >>> command.process(FakeArgs) - 3 matching mailing lists found: - list-one@example.com [List-one] - List One - list-one@example.net [List-one] - List One in Example.Net - list-two@example.com [List-two] - List Two - -Maybe you want the descriptions but not the names. - - >>> FakeArgs.names = False - >>> command.process(FakeArgs) - 3 matching mailing lists found: - list-one@example.com - List One - list-one@example.net - List One in Example.Net - list-two@example.com - List Two - - -Less verbosity -============== - -There's also a ``--quiet/-q`` switch which reduces the verbosity a bit. - - >>> FakeArgs.quiet = True - >>> FakeArgs.descriptions = False - >>> command.process(FakeArgs) - list-one@example.com - list-one@example.net - list-two@example.com - - -Specific domain -=============== - -You can narrow the search down to a specific domain with the --domain option. -A helpful message is displayed if no matching domains are given. - - >>> FakeArgs.quiet = False - >>> FakeArgs.domains = ['example.org'] - >>> command.process(FakeArgs) - No matching mailing lists found - -But if a matching domain is given, only mailing lists in that domain are -shown. - - >>> FakeArgs.domains = ['example.net'] - >>> command.process(FakeArgs) - 1 matching mailing lists found: - list-one@example.net - -More than one --domain argument can be given; then all mailing lists in -matching domains are shown. - - >>> FakeArgs.domains = ['example.com', 'example.net'] - >>> command.process(FakeArgs) - 3 matching mailing lists found: - list-one@example.com - list-one@example.net - list-two@example.com - - -Advertised lists -================ - -Mailing lists can be 'advertised' meaning their existence is public -knowledge. Non-advertised lists are considered private. Display through the -command line can select on this attribute. -:: - - >>> FakeArgs.domains = [] - >>> FakeArgs.advertised = True - >>> mlist_1.advertised = False - - >>> command.process(FakeArgs) - 2 matching mailing lists found: - list-one@example.net - list-two@example.com diff --git a/src/mailman/commands/docs/members.rst b/src/mailman/commands/docs/members.rst new file mode 100644 index 000000000..18a916781 --- /dev/null +++ b/src/mailman/commands/docs/members.rst @@ -0,0 +1,322 @@ +================ +Managing members +================ + +The ``bin/mailman members`` command allows a site administrator to display, +add, and remove members from a mailing list. +:: + + >>> mlist1 = create_list('test1@example.com') + + >>> class FakeArgs: + ... input_filename = None + ... output_filename = None + ... listname = [] + ... regular = False + ... digest = None + ... nomail = None + >>> args = FakeArgs() + + >>> from mailman.commands.cli_members import Members + >>> command = Members() + + +Listing members +=============== + +You can list all the members of a mailing list by calling the command with no +options. To start with, there are no members of the mailing list. + + >>> args.listname = [mlist1.fqdn_listname] + >>> command.process(args) + test1@example.com has no members + +Once the mailing list add some members, they will be displayed. +:: + + >>> from mailman.interfaces.member import DeliveryMode + >>> from mailman.app.membership import add_member + >>> add_member(mlist1, 'anne@example.com', 'Anne Person', 'xxx', + ... DeliveryMode.regular, mlist1.preferred_language.code) + + on test1@example.com as MemberRole.member> + >>> add_member(mlist1, 'bart@example.com', 'Bart Person', 'xxx', + ... DeliveryMode.regular, mlist1.preferred_language.code) + + on test1@example.com as MemberRole.member> + + >>> command.process(args) + Anne Person + Bart Person + +Members are displayed in alphabetical order based on their address. +:: + + >>> add_member(mlist1, 'anne@aaaxample.com', 'Anne Person', 'xxx', + ... DeliveryMode.regular, mlist1.preferred_language.code) + + on test1@example.com as MemberRole.member> + + >>> command.process(args) + Anne Person + Anne Person + Bart Person + +You can also output this list to a file. + + >>> from tempfile import mkstemp + >>> fd, args.output_filename = mkstemp() + >>> import os + >>> os.close(fd) + >>> command.process(args) + >>> with open(args.output_filename) as fp: + ... print fp.read() + Anne Person + Anne Person + Bart Person + >>> os.remove(args.output_filename) + >>> args.output_filename = None + +The output file can also be standard out. + + >>> args.output_filename = '-' + >>> command.process(args) + Anne Person + Anne Person + Bart Person + >>> args.output_filename = None + + +Filtering on delivery mode +-------------------------- + +You can limit output to just the regular non-digest members... + + >>> args.regular = True + >>> member = mlist1.members.get_member('anne@example.com') + >>> member.preferences.delivery_mode = DeliveryMode.plaintext_digests + >>> command.process(args) + Anne Person + Bart Person + +...or just the digest members. Furthermore, you can either display all digest +members... + + >>> member = mlist1.members.get_member('anne@aaaxample.com') + >>> member.preferences.delivery_mode = DeliveryMode.mime_digests + >>> args.regular = False + >>> args.digest = 'any' + >>> command.process(args) + Anne Person + Anne Person + +...just plain text digest members... + + >>> args.digest = 'plaintext' + >>> command.process(args) + Anne Person + +...just MIME digest members. +:: + + >>> args.digest = 'mime' + >>> command.process(args) + Anne Person + + # Reset for following tests. + >>> args.digest = None + + +Filtering on delivery status +---------------------------- + +You can also filter the display on the member's delivery status. By default, +all members are displayed, but you can filter out only those whose delivery +status is enabled... +:: + + >>> from mailman.interfaces.member import DeliveryStatus + >>> member = mlist1.members.get_member('anne@aaaxample.com') + >>> member.preferences.delivery_status = DeliveryStatus.by_moderator + >>> member = mlist1.members.get_member('bart@example.com') + >>> member.preferences.delivery_status = DeliveryStatus.by_user + >>> member = add_member( + ... mlist1, 'cris@example.com', 'Cris Person', 'xxx', + ... DeliveryMode.regular, mlist1.preferred_language.code) + >>> member.preferences.delivery_status = DeliveryStatus.unknown + >>> member = add_member( + ... mlist1, 'dave@example.com', 'Dave Person', 'xxx', + ... DeliveryMode.regular, mlist1.preferred_language.code) + >>> member.preferences.delivery_status = DeliveryStatus.enabled + >>> member = add_member( + ... mlist1, 'elly@example.com', 'Elly Person', 'xxx', + ... DeliveryMode.regular, mlist1.preferred_language.code) + >>> member.preferences.delivery_status = DeliveryStatus.by_bounces + + >>> args.nomail = 'enabled' + >>> command.process(args) + Anne Person + Dave Person + +...or disabled by the user... + + >>> args.nomail = 'byuser' + >>> command.process(args) + Bart Person + +...or disabled by the list administrator (or moderator)... + + >>> args.nomail = 'byadmin' + >>> command.process(args) + Anne Person + +...or by the bounce processor... + + >>> args.nomail = 'bybounces' + >>> command.process(args) + Elly Person + +...or for unknown (legacy) reasons. + + >>> args.nomail = 'unknown' + >>> command.process(args) + Cris Person + +You can also display all members who have delivery disabled for any reason. +:: + + >>> args.nomail = 'any' + >>> command.process(args) + Anne Person + Bart Person + Cris Person + Elly Person + + # Reset for following tests. + >>> args.nomail = None + + +Adding members +============== + +You can add members to a mailing list from the command line. To do so, you +need a file containing email addresses and full names that can be parsed by +``email.utils.parseaddr()``. +:: + + >>> mlist2 = create_list('test2@example.com') + + >>> import os + >>> path = os.path.join(config.VAR_DIR, 'addresses.txt') + >>> with open(path, 'w') as fp: + ... for address in ('aperson@example.com', + ... 'Bart Person ', + ... 'cperson@example.com (Cate Person)', + ... ): + ... print >> fp, address + + >>> args.input_filename = path + >>> args.listname = [mlist2.fqdn_listname] + >>> command.process(args) + + >>> from operator import attrgetter + >>> dump_list(mlist2.members.addresses, key=attrgetter('email')) + aperson@example.com + Bart Person + Cate Person + +You can also specify ``-`` as the filename, in which case the addresses are +taken from standard input. +:: + + >>> from StringIO import StringIO + >>> fp = StringIO() + >>> fp.encoding = 'us-ascii' + >>> for address in ('dperson@example.com', + ... 'Elly Person ', + ... 'fperson@example.com (Fred Person)', + ... ): + ... print >> fp, address + >>> fp.seek(0) + >>> import sys + >>> sys.stdin = fp + + >>> args.input_filename = '-' + >>> command.process(args) + >>> sys.stdin = sys.__stdin__ + + >>> dump_list(mlist2.members.addresses, key=attrgetter('email')) + aperson@example.com + Bart Person + Cate Person + dperson@example.com + Elly Person + Fred Person + +Blank lines and lines that begin with '#' are ignored. +:: + + >>> with open(path, 'w') as fp: + ... for address in ('gperson@example.com', + ... '# hperson@example.com', + ... ' ', + ... '', + ... 'iperson@example.com', + ... ): + ... print >> fp, address + + >>> args.input_filename = path + >>> command.process(args) + >>> dump_list(mlist2.members.addresses, key=attrgetter('email')) + aperson@example.com + Bart Person + Cate Person + dperson@example.com + Elly Person + Fred Person + gperson@example.com + iperson@example.com + +Addresses which are already subscribed are ignored, although a warning is +printed. +:: + + >>> with open(path, 'w') as fp: + ... for address in ('gperson@example.com', + ... 'aperson@example.com', + ... 'jperson@example.com', + ... ): + ... print >> fp, address + + >>> command.process(args) + Already subscribed (skipping): gperson@example.com + Already subscribed (skipping): aperson@example.com + + >>> dump_list(mlist2.members.addresses, key=attrgetter('email')) + aperson@example.com + Bart Person + Cate Person + dperson@example.com + Elly Person + Fred Person + gperson@example.com + iperson@example.com + jperson@example.com + + +Displaying members +================== + +With no arguments, the command displays all members of the list. + + >>> args.input_filename = None + >>> command.process(args) + aperson@example.com + Bart Person + Cate Person + dperson@example.com + Elly Person + Fred Person + gperson@example.com + iperson@example.com + jperson@example.com diff --git a/src/mailman/commands/docs/members.txt b/src/mailman/commands/docs/members.txt deleted file mode 100644 index 18a916781..000000000 --- a/src/mailman/commands/docs/members.txt +++ /dev/null @@ -1,322 +0,0 @@ -================ -Managing members -================ - -The ``bin/mailman members`` command allows a site administrator to display, -add, and remove members from a mailing list. -:: - - >>> mlist1 = create_list('test1@example.com') - - >>> class FakeArgs: - ... input_filename = None - ... output_filename = None - ... listname = [] - ... regular = False - ... digest = None - ... nomail = None - >>> args = FakeArgs() - - >>> from mailman.commands.cli_members import Members - >>> command = Members() - - -Listing members -=============== - -You can list all the members of a mailing list by calling the command with no -options. To start with, there are no members of the mailing list. - - >>> args.listname = [mlist1.fqdn_listname] - >>> command.process(args) - test1@example.com has no members - -Once the mailing list add some members, they will be displayed. -:: - - >>> from mailman.interfaces.member import DeliveryMode - >>> from mailman.app.membership import add_member - >>> add_member(mlist1, 'anne@example.com', 'Anne Person', 'xxx', - ... DeliveryMode.regular, mlist1.preferred_language.code) - - on test1@example.com as MemberRole.member> - >>> add_member(mlist1, 'bart@example.com', 'Bart Person', 'xxx', - ... DeliveryMode.regular, mlist1.preferred_language.code) - - on test1@example.com as MemberRole.member> - - >>> command.process(args) - Anne Person - Bart Person - -Members are displayed in alphabetical order based on their address. -:: - - >>> add_member(mlist1, 'anne@aaaxample.com', 'Anne Person', 'xxx', - ... DeliveryMode.regular, mlist1.preferred_language.code) - - on test1@example.com as MemberRole.member> - - >>> command.process(args) - Anne Person - Anne Person - Bart Person - -You can also output this list to a file. - - >>> from tempfile import mkstemp - >>> fd, args.output_filename = mkstemp() - >>> import os - >>> os.close(fd) - >>> command.process(args) - >>> with open(args.output_filename) as fp: - ... print fp.read() - Anne Person - Anne Person - Bart Person - >>> os.remove(args.output_filename) - >>> args.output_filename = None - -The output file can also be standard out. - - >>> args.output_filename = '-' - >>> command.process(args) - Anne Person - Anne Person - Bart Person - >>> args.output_filename = None - - -Filtering on delivery mode --------------------------- - -You can limit output to just the regular non-digest members... - - >>> args.regular = True - >>> member = mlist1.members.get_member('anne@example.com') - >>> member.preferences.delivery_mode = DeliveryMode.plaintext_digests - >>> command.process(args) - Anne Person - Bart Person - -...or just the digest members. Furthermore, you can either display all digest -members... - - >>> member = mlist1.members.get_member('anne@aaaxample.com') - >>> member.preferences.delivery_mode = DeliveryMode.mime_digests - >>> args.regular = False - >>> args.digest = 'any' - >>> command.process(args) - Anne Person - Anne Person - -...just plain text digest members... - - >>> args.digest = 'plaintext' - >>> command.process(args) - Anne Person - -...just MIME digest members. -:: - - >>> args.digest = 'mime' - >>> command.process(args) - Anne Person - - # Reset for following tests. - >>> args.digest = None - - -Filtering on delivery status ----------------------------- - -You can also filter the display on the member's delivery status. By default, -all members are displayed, but you can filter out only those whose delivery -status is enabled... -:: - - >>> from mailman.interfaces.member import DeliveryStatus - >>> member = mlist1.members.get_member('anne@aaaxample.com') - >>> member.preferences.delivery_status = DeliveryStatus.by_moderator - >>> member = mlist1.members.get_member('bart@example.com') - >>> member.preferences.delivery_status = DeliveryStatus.by_user - >>> member = add_member( - ... mlist1, 'cris@example.com', 'Cris Person', 'xxx', - ... DeliveryMode.regular, mlist1.preferred_language.code) - >>> member.preferences.delivery_status = DeliveryStatus.unknown - >>> member = add_member( - ... mlist1, 'dave@example.com', 'Dave Person', 'xxx', - ... DeliveryMode.regular, mlist1.preferred_language.code) - >>> member.preferences.delivery_status = DeliveryStatus.enabled - >>> member = add_member( - ... mlist1, 'elly@example.com', 'Elly Person', 'xxx', - ... DeliveryMode.regular, mlist1.preferred_language.code) - >>> member.preferences.delivery_status = DeliveryStatus.by_bounces - - >>> args.nomail = 'enabled' - >>> command.process(args) - Anne Person - Dave Person - -...or disabled by the user... - - >>> args.nomail = 'byuser' - >>> command.process(args) - Bart Person - -...or disabled by the list administrator (or moderator)... - - >>> args.nomail = 'byadmin' - >>> command.process(args) - Anne Person - -...or by the bounce processor... - - >>> args.nomail = 'bybounces' - >>> command.process(args) - Elly Person - -...or for unknown (legacy) reasons. - - >>> args.nomail = 'unknown' - >>> command.process(args) - Cris Person - -You can also display all members who have delivery disabled for any reason. -:: - - >>> args.nomail = 'any' - >>> command.process(args) - Anne Person - Bart Person - Cris Person - Elly Person - - # Reset for following tests. - >>> args.nomail = None - - -Adding members -============== - -You can add members to a mailing list from the command line. To do so, you -need a file containing email addresses and full names that can be parsed by -``email.utils.parseaddr()``. -:: - - >>> mlist2 = create_list('test2@example.com') - - >>> import os - >>> path = os.path.join(config.VAR_DIR, 'addresses.txt') - >>> with open(path, 'w') as fp: - ... for address in ('aperson@example.com', - ... 'Bart Person ', - ... 'cperson@example.com (Cate Person)', - ... ): - ... print >> fp, address - - >>> args.input_filename = path - >>> args.listname = [mlist2.fqdn_listname] - >>> command.process(args) - - >>> from operator import attrgetter - >>> dump_list(mlist2.members.addresses, key=attrgetter('email')) - aperson@example.com - Bart Person - Cate Person - -You can also specify ``-`` as the filename, in which case the addresses are -taken from standard input. -:: - - >>> from StringIO import StringIO - >>> fp = StringIO() - >>> fp.encoding = 'us-ascii' - >>> for address in ('dperson@example.com', - ... 'Elly Person ', - ... 'fperson@example.com (Fred Person)', - ... ): - ... print >> fp, address - >>> fp.seek(0) - >>> import sys - >>> sys.stdin = fp - - >>> args.input_filename = '-' - >>> command.process(args) - >>> sys.stdin = sys.__stdin__ - - >>> dump_list(mlist2.members.addresses, key=attrgetter('email')) - aperson@example.com - Bart Person - Cate Person - dperson@example.com - Elly Person - Fred Person - -Blank lines and lines that begin with '#' are ignored. -:: - - >>> with open(path, 'w') as fp: - ... for address in ('gperson@example.com', - ... '# hperson@example.com', - ... ' ', - ... '', - ... 'iperson@example.com', - ... ): - ... print >> fp, address - - >>> args.input_filename = path - >>> command.process(args) - >>> dump_list(mlist2.members.addresses, key=attrgetter('email')) - aperson@example.com - Bart Person - Cate Person - dperson@example.com - Elly Person - Fred Person - gperson@example.com - iperson@example.com - -Addresses which are already subscribed are ignored, although a warning is -printed. -:: - - >>> with open(path, 'w') as fp: - ... for address in ('gperson@example.com', - ... 'aperson@example.com', - ... 'jperson@example.com', - ... ): - ... print >> fp, address - - >>> command.process(args) - Already subscribed (skipping): gperson@example.com - Already subscribed (skipping): aperson@example.com - - >>> dump_list(mlist2.members.addresses, key=attrgetter('email')) - aperson@example.com - Bart Person - Cate Person - dperson@example.com - Elly Person - Fred Person - gperson@example.com - iperson@example.com - jperson@example.com - - -Displaying members -================== - -With no arguments, the command displays all members of the list. - - >>> args.input_filename = None - >>> command.process(args) - aperson@example.com - Bart Person - Cate Person - dperson@example.com - Elly Person - Fred Person - gperson@example.com - iperson@example.com - jperson@example.com diff --git a/src/mailman/commands/docs/membership.rst b/src/mailman/commands/docs/membership.rst new file mode 100644 index 000000000..d05f12eee --- /dev/null +++ b/src/mailman/commands/docs/membership.rst @@ -0,0 +1,372 @@ +============================ +Membership changes via email +============================ + +Membership changes such as joining and leaving a mailing list, can be effected +via the email interface. The Mailman email commands ``join``, ``leave``, and +``confirm`` are used. + + +Joining a mailing list +====================== + +The mail command ``join`` subscribes an email address to the mailing list. +``subscribe`` is an alias for ``join``. + + >>> from mailman.commands.eml_membership import Join + >>> join = Join() + >>> print join.name + join + >>> print join.description + Join this mailing list. You will be asked to confirm your subscription + request and you may be issued a provisional password. + + By using the 'digest' option, you can specify whether you want digest + delivery or not. If not specified, the mailing list's default will be + used. You can also subscribe an alternative address by using the + 'address' option. For example: + + join address=myotheraddress@example.com + + >>> print join.argument_description + [digest=] [address=
] + + +No address to join +------------------ + + >>> mlist = create_list('alpha@example.com') + +When no address argument is given, the message's From address will be used. +If that's missing though, then an error is returned. +:: + + >>> from mailman.runners.command import Results + >>> results = Results() + + >>> from mailman.email.message import Message + >>> print join.process(mlist, Message(), {}, (), results) + ContinueProcessing.no + >>> print unicode(results) + The results of your email command are provided below. + + join: No valid address found to subscribe + + +The ``subscribe`` command is an alias. + + >>> from mailman.commands.eml_membership import Subscribe + >>> subscribe = Subscribe() + >>> print subscribe.name + subscribe + >>> results = Results() + >>> print subscribe.process(mlist, Message(), {}, (), results) + ContinueProcessing.no + >>> print unicode(results) + The results of your email command are provided below. + + subscribe: No valid address found to subscribe + + + +Joining the sender +------------------ + +When the message has a From field, that address will be subscribed. + + >>> msg = message_from_string("""\ + ... From: Anne Person + ... + ... """) + >>> results = Results() + >>> print join.process(mlist, msg, {}, (), results) + ContinueProcessing.yes + >>> print unicode(results) + The results of your email command are provided below. + + Confirmation email sent to Anne Person + + +Anne is not yet a member because she must confirm her subscription request +first. + + >>> from mailman.interfaces.usermanager import IUserManager + >>> from zope.component import getUtility + >>> user_manager = getUtility(IUserManager) + >>> print user_manager.get_user('anne@example.com') + None + +Mailman has sent her the confirmation message. + + >>> from mailman.testing.helpers import get_queue_messages + >>> items = get_queue_messages('virgin') + >>> len(items) + 1 + >>> print items[0].msg.as_string() + MIME-Version: 1.0 + ... + Subject: confirm ... + From: alpha-confirm+...@example.com + To: anne@example.com + ... + + Email Address Registration Confirmation + + Hello, this is the GNU Mailman server at example.com. + + We have received a registration request for the email address + + anne@example.com + + Before you can start using GNU Mailman at this site, you must first + confirm that this is your email address. You can do this by replying to + this message, keeping the Subject header intact. Or you can visit this + web page + + http://lists.example.com/confirm/... + + If you do not wish to register this email address simply disregard this + message. If you think you are being maliciously subscribed to the list, or + have any other questions, you may contact + + postmaster@example.com + + +Once Anne confirms her registration, she will be made a member of the mailing +list. +:: + + >>> def extract_token(message): + ... return str(message['subject']).split()[1].strip() + >>> token = extract_token(items[0].msg) + + >>> from mailman.commands.eml_confirm import Confirm + >>> confirm = Confirm() + >>> msg = message_from_string("""\ + ... To: alpha-confirm+{token}@example.com + ... From: anne@example.com + ... Subject: Re: confirm {token} + ... + ... """.format(token=token)) + + >>> results = Results() + >>> print confirm.process(mlist, msg, {}, (token,), results) + ContinueProcessing.yes + >>> print unicode(results) + The results of your email command are provided below. + + Confirmed + + + >>> user = user_manager.get_user('anne@example.com') + >>> print user.real_name + Anne Person + >>> list(user.addresses) + [ [verified] at ...>] + +Anne is also now a member of the mailing list. + + >>> mlist.members.get_member('anne@example.com') + + on alpha@example.com as MemberRole.member> + + +Joining a second list +--------------------- + + >>> mlist_2 = create_list('baker@example.com') + >>> msg = message_from_string("""\ + ... From: Anne Person + ... + ... """) + >>> print join.process(mlist_2, msg, {}, (), Results()) + ContinueProcessing.yes + +Anne of course, is still registered. + + >>> print user_manager.get_user('anne@example.com') + + +But she is not a member of the mailing list. + + >>> print mlist_2.members.get_member('anne@example.com') + None + +One Anne confirms this subscription, she becomes a member of the mailing +list. +:: + + >>> items = get_queue_messages('virgin') + >>> len(items) + 1 + >>> token = extract_token(items[0].msg) + >>> msg = message_from_string("""\ + ... To: baker-confirm+{token}@example.com + ... From: anne@example.com + ... Subject: Re: confirm {token} + ... + ... """.format(token=token)) + + >>> results = Results() + >>> print confirm.process(mlist_2, msg, {}, (token,), results) + ContinueProcessing.yes + >>> print unicode(results) + The results of your email command are provided below. + + Confirmed + + + >>> print mlist_2.members.get_member('anne@example.com') + + on baker@example.com as MemberRole.member> + + +Leaving a mailing list +====================== + +The mail command ``leave`` unsubscribes an email address from the mailing +list. ``unsubscribe`` is an alias for ``leave``. + + >>> from mailman.commands.eml_membership import Leave + >>> leave = Leave() + >>> print leave.name + leave + >>> print leave.description + Leave this mailing list. You will be asked to confirm your request. + +Anne is a member of the ``baker@example.com`` mailing list, when she decides +to leave it. She sends a message to the ``-leave`` address for the list and +is sent a confirmation message for her request. + + >>> results = Results() + >>> print leave.process(mlist_2, msg, {}, (), results) + ContinueProcessing.yes + >>> print unicode(results) + The results of your email command are provided below. + + Anne Person left baker@example.com + + +Anne is no longer a member of the mailing list. + + >>> print mlist_2.members.get_member('anne@example.com') + None + +Anne does not need to leave a mailing list with the same email address she's +subscribe with. Any of her registered, linked, and validated email addresses +will do. +:: + + >>> anne = user_manager.get_user('anne@example.com') + >>> address = anne.register('anne.person@example.org') + + >>> results = Results() + >>> print mlist.members.get_member('anne@example.com') + + on alpha@example.com as MemberRole.member> + + >>> msg = message_from_string("""\ + ... To: alpha-leave@example.com + ... From: anne.person@example.org + ... + ... """) + +Since Anne's alternative address has not yet been verified, it can't be used +to unsubscribe Anne from the alpha mailing list. +:: + + >>> print leave.process(mlist, msg, {}, (), results) + ContinueProcessing.no + + >>> print unicode(results) + The results of your email command are provided below. + + Invalid or unverified email address: anne.person@example.org + + + >>> print mlist.members.get_member('anne@example.com') + + on alpha@example.com as MemberRole.member> + +Once Anne has verified her alternative address though, it can be used to +unsubscribe her from the list. +:: + + >>> from datetime import datetime + >>> address.verified_on = datetime.now() + + >>> results = Results() + >>> print leave.process(mlist, msg, {}, (), results) + ContinueProcessing.yes + + >>> print unicode(results) + The results of your email command are provided below. + + Anne Person left alpha@example.com + + + >>> print mlist.members.get_member('anne@example.com') + None + + +Confirmations +============= + +Bart wants to join the alpha list, so he sends his subscription request. +:: + + >>> msg = message_from_string("""\ + ... From: Bart Person + ... + ... """) + + >>> print join.process(mlist, msg, {}, (), Results()) + ContinueProcessing.yes + +There are two messages in the virgin queue, one of which is the confirmation +message. + + >>> for item in get_queue_messages('virgin'): + ... if str(item.msg['subject']).startswith('confirm'): + ... break + ... else: + ... raise AssertionError('No confirmation message') + >>> token = extract_token(item.msg) + +Bart is still not a user. + + >>> print user_manager.get_user('bart@example.com') + None + +Bart replies to the original message, specifically keeping the Subject header +intact except for any prefix. Mailman matches the token and confirms Bart as +a user of the system. +:: + + >>> msg = message_from_string("""\ + ... From: Bart Person + ... To: alpha-confirm+{token}@example.com + ... Subject: Re: confirm {token} + ... + ... """.format(token=token)) + + >>> results = Results() + >>> print confirm.process(mlist, msg, {}, (token,), results) + ContinueProcessing.yes + + >>> print unicode(results) + The results of your email command are provided below. + + Confirmed + + +Now Bart is a user... + + >>> print user_manager.get_user('bart@example.com') + + +...and a member of the mailing list. + + >>> print mlist.members.get_member('bart@example.com') + + on alpha@example.com as MemberRole.member> diff --git a/src/mailman/commands/docs/membership.txt b/src/mailman/commands/docs/membership.txt deleted file mode 100644 index d05f12eee..000000000 --- a/src/mailman/commands/docs/membership.txt +++ /dev/null @@ -1,372 +0,0 @@ -============================ -Membership changes via email -============================ - -Membership changes such as joining and leaving a mailing list, can be effected -via the email interface. The Mailman email commands ``join``, ``leave``, and -``confirm`` are used. - - -Joining a mailing list -====================== - -The mail command ``join`` subscribes an email address to the mailing list. -``subscribe`` is an alias for ``join``. - - >>> from mailman.commands.eml_membership import Join - >>> join = Join() - >>> print join.name - join - >>> print join.description - Join this mailing list. You will be asked to confirm your subscription - request and you may be issued a provisional password. - - By using the 'digest' option, you can specify whether you want digest - delivery or not. If not specified, the mailing list's default will be - used. You can also subscribe an alternative address by using the - 'address' option. For example: - - join address=myotheraddress@example.com - - >>> print join.argument_description - [digest=] [address=
] - - -No address to join ------------------- - - >>> mlist = create_list('alpha@example.com') - -When no address argument is given, the message's From address will be used. -If that's missing though, then an error is returned. -:: - - >>> from mailman.runners.command import Results - >>> results = Results() - - >>> from mailman.email.message import Message - >>> print join.process(mlist, Message(), {}, (), results) - ContinueProcessing.no - >>> print unicode(results) - The results of your email command are provided below. - - join: No valid address found to subscribe - - -The ``subscribe`` command is an alias. - - >>> from mailman.commands.eml_membership import Subscribe - >>> subscribe = Subscribe() - >>> print subscribe.name - subscribe - >>> results = Results() - >>> print subscribe.process(mlist, Message(), {}, (), results) - ContinueProcessing.no - >>> print unicode(results) - The results of your email command are provided below. - - subscribe: No valid address found to subscribe - - - -Joining the sender ------------------- - -When the message has a From field, that address will be subscribed. - - >>> msg = message_from_string("""\ - ... From: Anne Person - ... - ... """) - >>> results = Results() - >>> print join.process(mlist, msg, {}, (), results) - ContinueProcessing.yes - >>> print unicode(results) - The results of your email command are provided below. - - Confirmation email sent to Anne Person - - -Anne is not yet a member because she must confirm her subscription request -first. - - >>> from mailman.interfaces.usermanager import IUserManager - >>> from zope.component import getUtility - >>> user_manager = getUtility(IUserManager) - >>> print user_manager.get_user('anne@example.com') - None - -Mailman has sent her the confirmation message. - - >>> from mailman.testing.helpers import get_queue_messages - >>> items = get_queue_messages('virgin') - >>> len(items) - 1 - >>> print items[0].msg.as_string() - MIME-Version: 1.0 - ... - Subject: confirm ... - From: alpha-confirm+...@example.com - To: anne@example.com - ... - - Email Address Registration Confirmation - - Hello, this is the GNU Mailman server at example.com. - - We have received a registration request for the email address - - anne@example.com - - Before you can start using GNU Mailman at this site, you must first - confirm that this is your email address. You can do this by replying to - this message, keeping the Subject header intact. Or you can visit this - web page - - http://lists.example.com/confirm/... - - If you do not wish to register this email address simply disregard this - message. If you think you are being maliciously subscribed to the list, or - have any other questions, you may contact - - postmaster@example.com - - -Once Anne confirms her registration, she will be made a member of the mailing -list. -:: - - >>> def extract_token(message): - ... return str(message['subject']).split()[1].strip() - >>> token = extract_token(items[0].msg) - - >>> from mailman.commands.eml_confirm import Confirm - >>> confirm = Confirm() - >>> msg = message_from_string("""\ - ... To: alpha-confirm+{token}@example.com - ... From: anne@example.com - ... Subject: Re: confirm {token} - ... - ... """.format(token=token)) - - >>> results = Results() - >>> print confirm.process(mlist, msg, {}, (token,), results) - ContinueProcessing.yes - >>> print unicode(results) - The results of your email command are provided below. - - Confirmed - - - >>> user = user_manager.get_user('anne@example.com') - >>> print user.real_name - Anne Person - >>> list(user.addresses) - [ [verified] at ...>] - -Anne is also now a member of the mailing list. - - >>> mlist.members.get_member('anne@example.com') - - on alpha@example.com as MemberRole.member> - - -Joining a second list ---------------------- - - >>> mlist_2 = create_list('baker@example.com') - >>> msg = message_from_string("""\ - ... From: Anne Person - ... - ... """) - >>> print join.process(mlist_2, msg, {}, (), Results()) - ContinueProcessing.yes - -Anne of course, is still registered. - - >>> print user_manager.get_user('anne@example.com') - - -But she is not a member of the mailing list. - - >>> print mlist_2.members.get_member('anne@example.com') - None - -One Anne confirms this subscription, she becomes a member of the mailing -list. -:: - - >>> items = get_queue_messages('virgin') - >>> len(items) - 1 - >>> token = extract_token(items[0].msg) - >>> msg = message_from_string("""\ - ... To: baker-confirm+{token}@example.com - ... From: anne@example.com - ... Subject: Re: confirm {token} - ... - ... """.format(token=token)) - - >>> results = Results() - >>> print confirm.process(mlist_2, msg, {}, (token,), results) - ContinueProcessing.yes - >>> print unicode(results) - The results of your email command are provided below. - - Confirmed - - - >>> print mlist_2.members.get_member('anne@example.com') - - on baker@example.com as MemberRole.member> - - -Leaving a mailing list -====================== - -The mail command ``leave`` unsubscribes an email address from the mailing -list. ``unsubscribe`` is an alias for ``leave``. - - >>> from mailman.commands.eml_membership import Leave - >>> leave = Leave() - >>> print leave.name - leave - >>> print leave.description - Leave this mailing list. You will be asked to confirm your request. - -Anne is a member of the ``baker@example.com`` mailing list, when she decides -to leave it. She sends a message to the ``-leave`` address for the list and -is sent a confirmation message for her request. - - >>> results = Results() - >>> print leave.process(mlist_2, msg, {}, (), results) - ContinueProcessing.yes - >>> print unicode(results) - The results of your email command are provided below. - - Anne Person left baker@example.com - - -Anne is no longer a member of the mailing list. - - >>> print mlist_2.members.get_member('anne@example.com') - None - -Anne does not need to leave a mailing list with the same email address she's -subscribe with. Any of her registered, linked, and validated email addresses -will do. -:: - - >>> anne = user_manager.get_user('anne@example.com') - >>> address = anne.register('anne.person@example.org') - - >>> results = Results() - >>> print mlist.members.get_member('anne@example.com') - - on alpha@example.com as MemberRole.member> - - >>> msg = message_from_string("""\ - ... To: alpha-leave@example.com - ... From: anne.person@example.org - ... - ... """) - -Since Anne's alternative address has not yet been verified, it can't be used -to unsubscribe Anne from the alpha mailing list. -:: - - >>> print leave.process(mlist, msg, {}, (), results) - ContinueProcessing.no - - >>> print unicode(results) - The results of your email command are provided below. - - Invalid or unverified email address: anne.person@example.org - - - >>> print mlist.members.get_member('anne@example.com') - - on alpha@example.com as MemberRole.member> - -Once Anne has verified her alternative address though, it can be used to -unsubscribe her from the list. -:: - - >>> from datetime import datetime - >>> address.verified_on = datetime.now() - - >>> results = Results() - >>> print leave.process(mlist, msg, {}, (), results) - ContinueProcessing.yes - - >>> print unicode(results) - The results of your email command are provided below. - - Anne Person left alpha@example.com - - - >>> print mlist.members.get_member('anne@example.com') - None - - -Confirmations -============= - -Bart wants to join the alpha list, so he sends his subscription request. -:: - - >>> msg = message_from_string("""\ - ... From: Bart Person - ... - ... """) - - >>> print join.process(mlist, msg, {}, (), Results()) - ContinueProcessing.yes - -There are two messages in the virgin queue, one of which is the confirmation -message. - - >>> for item in get_queue_messages('virgin'): - ... if str(item.msg['subject']).startswith('confirm'): - ... break - ... else: - ... raise AssertionError('No confirmation message') - >>> token = extract_token(item.msg) - -Bart is still not a user. - - >>> print user_manager.get_user('bart@example.com') - None - -Bart replies to the original message, specifically keeping the Subject header -intact except for any prefix. Mailman matches the token and confirms Bart as -a user of the system. -:: - - >>> msg = message_from_string("""\ - ... From: Bart Person - ... To: alpha-confirm+{token}@example.com - ... Subject: Re: confirm {token} - ... - ... """.format(token=token)) - - >>> results = Results() - >>> print confirm.process(mlist, msg, {}, (token,), results) - ContinueProcessing.yes - - >>> print unicode(results) - The results of your email command are provided below. - - Confirmed - - -Now Bart is a user... - - >>> print user_manager.get_user('bart@example.com') - - -...and a member of the mailing list. - - >>> print mlist.members.get_member('bart@example.com') - - on alpha@example.com as MemberRole.member> diff --git a/src/mailman/commands/docs/qfile.rst b/src/mailman/commands/docs/qfile.rst new file mode 100644 index 000000000..74ede1b64 --- /dev/null +++ b/src/mailman/commands/docs/qfile.rst @@ -0,0 +1,70 @@ +=================== +Dumping queue files +=================== + +The ``qfile`` command dumps the contents of a queue pickle file. This is +especially useful when you have shunt files you want to inspect. + +XXX Test the interactive operation of qfile + + +Pretty printing +=============== + +By default, the ``qfile`` command pretty prints the contents of a queue pickle +file to standard output. +:: + + >>> from mailman.commands.cli_qfile import QFile + >>> command = QFile() + + >>> class FakeArgs: + ... interactive = False + ... doprint = True + ... qfile = [] + +Let's say Mailman shunted a message file. +:: + + >>> msg = message_from_string("""\ + ... From: aperson@example.com + ... To: test@example.com + ... Subject: Uh oh + ... + ... I borkeded Mailman. + ... """) + + >>> shuntq = config.switchboards['shunt'] + >>> basename = shuntq.enqueue(msg, foo=7, bar='baz', bad='yes') + +Once we've figured out the file name of the shunted message, we can print it. +:: + + >>> from os.path import join + >>> qfile = join(shuntq.queue_directory, basename + '.pck') + + >>> FakeArgs.qfile = [qfile] + >>> command.process(FakeArgs) + [----- start pickle -----] + <----- start object 1 -----> + From nobody ... + From: aperson@example.com + To: test@example.com + Subject: Uh oh + + I borkeded Mailman. + + <----- start object 2 -----> + { u'_parsemsg': False, + 'bad': u'yes', + 'bar': u'baz', + 'foo': 7, + u'received_time': ... + u'version': 3} + [----- end pickle -----] + +Maybe we don't want to print the contents of the file though, in case we want +to enter the interactive prompt. + + >>> FakeArgs.doprint = False + >>> command.process(FakeArgs) diff --git a/src/mailman/commands/docs/qfile.txt b/src/mailman/commands/docs/qfile.txt deleted file mode 100644 index 74ede1b64..000000000 --- a/src/mailman/commands/docs/qfile.txt +++ /dev/null @@ -1,70 +0,0 @@ -=================== -Dumping queue files -=================== - -The ``qfile`` command dumps the contents of a queue pickle file. This is -especially useful when you have shunt files you want to inspect. - -XXX Test the interactive operation of qfile - - -Pretty printing -=============== - -By default, the ``qfile`` command pretty prints the contents of a queue pickle -file to standard output. -:: - - >>> from mailman.commands.cli_qfile import QFile - >>> command = QFile() - - >>> class FakeArgs: - ... interactive = False - ... doprint = True - ... qfile = [] - -Let's say Mailman shunted a message file. -:: - - >>> msg = message_from_string("""\ - ... From: aperson@example.com - ... To: test@example.com - ... Subject: Uh oh - ... - ... I borkeded Mailman. - ... """) - - >>> shuntq = config.switchboards['shunt'] - >>> basename = shuntq.enqueue(msg, foo=7, bar='baz', bad='yes') - -Once we've figured out the file name of the shunted message, we can print it. -:: - - >>> from os.path import join - >>> qfile = join(shuntq.queue_directory, basename + '.pck') - - >>> FakeArgs.qfile = [qfile] - >>> command.process(FakeArgs) - [----- start pickle -----] - <----- start object 1 -----> - From nobody ... - From: aperson@example.com - To: test@example.com - Subject: Uh oh - - I borkeded Mailman. - - <----- start object 2 -----> - { u'_parsemsg': False, - 'bad': u'yes', - 'bar': u'baz', - 'foo': 7, - u'received_time': ... - u'version': 3} - [----- end pickle -----] - -Maybe we don't want to print the contents of the file though, in case we want -to enter the interactive prompt. - - >>> FakeArgs.doprint = False - >>> command.process(FakeArgs) diff --git a/src/mailman/commands/docs/remove.rst b/src/mailman/commands/docs/remove.rst new file mode 100644 index 000000000..f0f4e64f6 --- /dev/null +++ b/src/mailman/commands/docs/remove.rst @@ -0,0 +1,85 @@ +========================= +Command line list removal +========================= + +A system administrator can remove mailing lists by the command line. +:: + + >>> create_list('test@example.com') + + + >>> from mailman.interfaces.listmanager import IListManager + >>> from zope.component import getUtility + >>> list_manager = getUtility(IListManager) + >>> list_manager.get('test@example.com') + + + >>> class FakeArgs: + ... quiet = False + ... archives = False + ... listname = ['test@example.com'] + >>> args = FakeArgs() + + >>> from mailman.commands.cli_lists import Remove + >>> command = Remove() + >>> command.process(args) + Removed list: test@example.com + Not removing archives. Reinvoke with -a to remove them. + + >>> print list_manager.get('test@example.com') + None + +You can also remove lists quietly. +:: + + >>> create_list('test@example.com') + + + >>> args.quiet = True + >>> command.process(args) + + >>> print list_manager.get('test@example.com') + None + + +Removing archives +================= + +By default 'mailman remove' does not remove a mailing list's archives. +:: + + >>> create_list('test@example.com') + + + # Fake an mbox file for the mailing list. + >>> import os + >>> def make_mbox(fqdn_listname): + ... mbox_dir = os.path.join( + ... config.PUBLIC_ARCHIVE_FILE_DIR, fqdn_listname + '.mbox') + ... os.makedirs(mbox_dir) + ... mbox_file = os.path.join(mbox_dir, fqdn_listname + '.mbox') + ... with open(mbox_file, 'w') as fp: + ... print >> fp, 'A message' + ... assert os.path.exists(mbox_file) + ... return mbox_file + + >>> mbox_file = make_mbox('test@example.com') + >>> args.quiet = False + >>> command.process(args) + Removed list: test@example.com + Not removing archives. Reinvoke with -a to remove them. + + >>> os.path.exists(mbox_file) + True + +Even if the mailing list has been deleted, you can still delete the archives +afterward. +:: + + >>> args.archives = True + + >>> command.process(args) + No such list: test@example.com; removing residual archives. + + >>> os.path.exists(mbox_file) + False diff --git a/src/mailman/commands/docs/remove.txt b/src/mailman/commands/docs/remove.txt deleted file mode 100644 index f0f4e64f6..000000000 --- a/src/mailman/commands/docs/remove.txt +++ /dev/null @@ -1,85 +0,0 @@ -========================= -Command line list removal -========================= - -A system administrator can remove mailing lists by the command line. -:: - - >>> create_list('test@example.com') - - - >>> from mailman.interfaces.listmanager import IListManager - >>> from zope.component import getUtility - >>> list_manager = getUtility(IListManager) - >>> list_manager.get('test@example.com') - - - >>> class FakeArgs: - ... quiet = False - ... archives = False - ... listname = ['test@example.com'] - >>> args = FakeArgs() - - >>> from mailman.commands.cli_lists import Remove - >>> command = Remove() - >>> command.process(args) - Removed list: test@example.com - Not removing archives. Reinvoke with -a to remove them. - - >>> print list_manager.get('test@example.com') - None - -You can also remove lists quietly. -:: - - >>> create_list('test@example.com') - - - >>> args.quiet = True - >>> command.process(args) - - >>> print list_manager.get('test@example.com') - None - - -Removing archives -================= - -By default 'mailman remove' does not remove a mailing list's archives. -:: - - >>> create_list('test@example.com') - - - # Fake an mbox file for the mailing list. - >>> import os - >>> def make_mbox(fqdn_listname): - ... mbox_dir = os.path.join( - ... config.PUBLIC_ARCHIVE_FILE_DIR, fqdn_listname + '.mbox') - ... os.makedirs(mbox_dir) - ... mbox_file = os.path.join(mbox_dir, fqdn_listname + '.mbox') - ... with open(mbox_file, 'w') as fp: - ... print >> fp, 'A message' - ... assert os.path.exists(mbox_file) - ... return mbox_file - - >>> mbox_file = make_mbox('test@example.com') - >>> args.quiet = False - >>> command.process(args) - Removed list: test@example.com - Not removing archives. Reinvoke with -a to remove them. - - >>> os.path.exists(mbox_file) - True - -Even if the mailing list has been deleted, you can still delete the archives -afterward. -:: - - >>> args.archives = True - - >>> command.process(args) - No such list: test@example.com; removing residual archives. - - >>> os.path.exists(mbox_file) - False diff --git a/src/mailman/commands/docs/status.rst b/src/mailman/commands/docs/status.rst new file mode 100644 index 000000000..7587157bc --- /dev/null +++ b/src/mailman/commands/docs/status.rst @@ -0,0 +1,37 @@ +============== +Getting status +============== + +The status of the Mailman master process can be queried from the command line. +It's clear at this point that nothing is running. +:: + + >>> from mailman.commands.cli_status import Status + >>> status = Status() + + >>> class FakeArgs: + ... pass + +The status is printed to stdout and a status code is returned. + + >>> status.process(FakeArgs) + GNU Mailman is not running + 0 + +We can simulate the master starting up by acquiring its lock. + + >>> from flufl.lock import Lock + >>> lock = Lock(config.LOCK_FILE) + >>> lock.lock() + +Getting the status confirms that the master is running. + + >>> status.process(FakeArgs) + GNU Mailman is running (master pid: ... + +We shut down the master and confirm the status. + + >>> lock.unlock() + >>> status.process(FakeArgs) + GNU Mailman is not running + 0 diff --git a/src/mailman/commands/docs/status.txt b/src/mailman/commands/docs/status.txt deleted file mode 100644 index 7587157bc..000000000 --- a/src/mailman/commands/docs/status.txt +++ /dev/null @@ -1,37 +0,0 @@ -============== -Getting status -============== - -The status of the Mailman master process can be queried from the command line. -It's clear at this point that nothing is running. -:: - - >>> from mailman.commands.cli_status import Status - >>> status = Status() - - >>> class FakeArgs: - ... pass - -The status is printed to stdout and a status code is returned. - - >>> status.process(FakeArgs) - GNU Mailman is not running - 0 - -We can simulate the master starting up by acquiring its lock. - - >>> from flufl.lock import Lock - >>> lock = Lock(config.LOCK_FILE) - >>> lock.lock() - -Getting the status confirms that the master is running. - - >>> status.process(FakeArgs) - GNU Mailman is running (master pid: ... - -We shut down the master and confirm the status. - - >>> lock.unlock() - >>> status.process(FakeArgs) - GNU Mailman is not running - 0 diff --git a/src/mailman/commands/docs/unshunt.rst b/src/mailman/commands/docs/unshunt.rst new file mode 100644 index 000000000..ce9d70316 --- /dev/null +++ b/src/mailman/commands/docs/unshunt.rst @@ -0,0 +1,155 @@ +======= +Unshunt +======= + +When errors occur while processing email messages, the messages will end up in +the ``shunt`` queue. The ``unshunt`` command allows system administrators to +manage the shunt queue. +:: + + >>> from mailman.commands.cli_unshunt import Unshunt + >>> command = Unshunt() + + >>> class FakeArgs: + ... discard = False + +Let's say there is a message in the shunt queue. +:: + + >>> msg = message_from_string("""\ + ... From: aperson@example.com + ... To: test@example.com + ... Subject: A broken message + ... Message-ID: + ... + ... """) + + >>> shuntq = config.switchboards['shunt'] + >>> len(list(shuntq.files)) + 0 + >>> base_name = shuntq.enqueue(msg, {}) + >>> len(list(shuntq.files)) + 1 + +The ``unshunt`` command by default moves the message back to the incoming +queue. +:: + + >>> inq = config.switchboards['in'] + >>> len(list(inq.files)) + 0 + + >>> command.process(FakeArgs) + + >>> from mailman.testing.helpers import get_queue_messages + >>> items = get_queue_messages('in') + >>> len(items) + 1 + >>> print items[0].msg.as_string() + From: aperson@example.com + To: test@example.com + Subject: A broken message + Message-ID: + + + +``unshunt`` moves all shunt queue messages. +:: + + >>> msg = message_from_string("""\ + ... From: aperson@example.com + ... To: test@example.com + ... Subject: A broken message + ... Message-ID: + ... + ... """) + >>> base_name = shuntq.enqueue(msg, {}) + + >>> msg = message_from_string("""\ + ... From: aperson@example.com + ... To: test@example.com + ... Subject: A broken message + ... Message-ID: + ... + ... """) + >>> base_name = shuntq.enqueue(msg, {}) + + >>> len(list(shuntq.files)) + 2 + + >>> command.process(FakeArgs) + >>> items = get_queue_messages('in') + >>> len(items) + 2 + + >>> sorted(item.msg['message-id'] for item in items) + [u'', u''] + + +Return to the original queue +============================ + +While the messages in the shunt queue are generally returned to the incoming +queue, if the error occurred while the message was being processed from a +different queue, it will be returned to the queue it came from. + + >>> msg = message_from_string("""\ + ... From: aperson@example.com + ... To: test@example.com + ... Subject: A broken message + ... Message-ID: + ... + ... """) + +The queue that the message comes from is in message metadata. +:: + + >>> base_name = shuntq.enqueue(msg, {}, whichq='bounces') + + >>> len(list(shuntq.files)) + 1 + >>> len(list(config.switchboards['bounces'].files)) + 0 + +The message is automatically re-queued to the bounces queue. +:: + + >>> command.process(FakeArgs) + >>> len(list(shuntq.files)) + 0 + >>> items = get_queue_messages('bounces') + >>> len(items) + 1 + + >>> print items[0].msg.as_string() + From: aperson@example.com + To: test@example.com + Subject: A broken message + Message-ID: + + + + +Discarding all shunted messages +=============================== + +If you don't care about the shunted messages, just discard them. +:: + + >>> msg = message_from_string("""\ + ... From: aperson@example.com + ... To: test@example.com + ... Subject: A broken message + ... Message-ID: + ... + ... """) + >>> base_name = shuntq.enqueue(msg, {}) + + >>> FakeArgs.discard = True + >>> command.process(FakeArgs) + +The messages are now gone. + + >>> items = get_queue_messages('in') + >>> len(items) + 0 diff --git a/src/mailman/commands/docs/unshunt.txt b/src/mailman/commands/docs/unshunt.txt deleted file mode 100644 index ce9d70316..000000000 --- a/src/mailman/commands/docs/unshunt.txt +++ /dev/null @@ -1,155 +0,0 @@ -======= -Unshunt -======= - -When errors occur while processing email messages, the messages will end up in -the ``shunt`` queue. The ``unshunt`` command allows system administrators to -manage the shunt queue. -:: - - >>> from mailman.commands.cli_unshunt import Unshunt - >>> command = Unshunt() - - >>> class FakeArgs: - ... discard = False - -Let's say there is a message in the shunt queue. -:: - - >>> msg = message_from_string("""\ - ... From: aperson@example.com - ... To: test@example.com - ... Subject: A broken message - ... Message-ID: - ... - ... """) - - >>> shuntq = config.switchboards['shunt'] - >>> len(list(shuntq.files)) - 0 - >>> base_name = shuntq.enqueue(msg, {}) - >>> len(list(shuntq.files)) - 1 - -The ``unshunt`` command by default moves the message back to the incoming -queue. -:: - - >>> inq = config.switchboards['in'] - >>> len(list(inq.files)) - 0 - - >>> command.process(FakeArgs) - - >>> from mailman.testing.helpers import get_queue_messages - >>> items = get_queue_messages('in') - >>> len(items) - 1 - >>> print items[0].msg.as_string() - From: aperson@example.com - To: test@example.com - Subject: A broken message - Message-ID: - - - -``unshunt`` moves all shunt queue messages. -:: - - >>> msg = message_from_string("""\ - ... From: aperson@example.com - ... To: test@example.com - ... Subject: A broken message - ... Message-ID: - ... - ... """) - >>> base_name = shuntq.enqueue(msg, {}) - - >>> msg = message_from_string("""\ - ... From: aperson@example.com - ... To: test@example.com - ... Subject: A broken message - ... Message-ID: - ... - ... """) - >>> base_name = shuntq.enqueue(msg, {}) - - >>> len(list(shuntq.files)) - 2 - - >>> command.process(FakeArgs) - >>> items = get_queue_messages('in') - >>> len(items) - 2 - - >>> sorted(item.msg['message-id'] for item in items) - [u'', u''] - - -Return to the original queue -============================ - -While the messages in the shunt queue are generally returned to the incoming -queue, if the error occurred while the message was being processed from a -different queue, it will be returned to the queue it came from. - - >>> msg = message_from_string("""\ - ... From: aperson@example.com - ... To: test@example.com - ... Subject: A broken message - ... Message-ID: - ... - ... """) - -The queue that the message comes from is in message metadata. -:: - - >>> base_name = shuntq.enqueue(msg, {}, whichq='bounces') - - >>> len(list(shuntq.files)) - 1 - >>> len(list(config.switchboards['bounces'].files)) - 0 - -The message is automatically re-queued to the bounces queue. -:: - - >>> command.process(FakeArgs) - >>> len(list(shuntq.files)) - 0 - >>> items = get_queue_messages('bounces') - >>> len(items) - 1 - - >>> print items[0].msg.as_string() - From: aperson@example.com - To: test@example.com - Subject: A broken message - Message-ID: - - - - -Discarding all shunted messages -=============================== - -If you don't care about the shunted messages, just discard them. -:: - - >>> msg = message_from_string("""\ - ... From: aperson@example.com - ... To: test@example.com - ... Subject: A broken message - ... Message-ID: - ... - ... """) - >>> base_name = shuntq.enqueue(msg, {}) - - >>> FakeArgs.discard = True - >>> command.process(FakeArgs) - -The messages are now gone. - - >>> items = get_queue_messages('in') - >>> len(items) - 0 diff --git a/src/mailman/commands/docs/version.rst b/src/mailman/commands/docs/version.rst new file mode 100644 index 000000000..8032df20a --- /dev/null +++ b/src/mailman/commands/docs/version.rst @@ -0,0 +1,12 @@ +==================== +Printing the version +==================== + +You can print the Mailman version number. +:: + + >>> from mailman.commands.cli_version import Version + >>> command = Version() + + >>> command.process(None) + GNU Mailman 3... diff --git a/src/mailman/commands/docs/version.txt b/src/mailman/commands/docs/version.txt deleted file mode 100644 index 8032df20a..000000000 --- a/src/mailman/commands/docs/version.txt +++ /dev/null @@ -1,12 +0,0 @@ -==================== -Printing the version -==================== - -You can print the Mailman version number. -:: - - >>> from mailman.commands.cli_version import Version - >>> command = Version() - - >>> command.process(None) - GNU Mailman 3... diff --git a/src/mailman/commands/docs/withlist.rst b/src/mailman/commands/docs/withlist.rst new file mode 100644 index 000000000..7632c726a --- /dev/null +++ b/src/mailman/commands/docs/withlist.rst @@ -0,0 +1,125 @@ +========================== +Operating on mailing lists +========================== + +The ``withlist`` command is a pretty powerful way to operate on mailing lists +from the command line. This command allows you to interact with a list at a +Python prompt, or process one or more mailing lists through custom made Python +functions. + +XXX Test the interactive operation of withlist + + +Getting detailed help +===================== + +Because ``withlist`` is so complex, you need to request detailed help. +:: + + >>> from mailman.commands.cli_withlist import Withlist + >>> command = Withlist() + + >>> class FakeArgs: + ... interactive = False + ... run = None + ... details = True + ... listname = [] + + >>> class FakeParser: + ... def error(self, message): + ... print message + >>> command.parser = FakeParser() + + >>> args = FakeArgs() + >>> command.process(args) + This script provides you with a general framework for interacting with a + mailing list. + ... + + +Running a command +================= + +By putting a Python function somewhere on your ``sys.path``, you can have +``withlist`` call that function on a given mailing list. The function takes a +single argument, the mailing list. +:: + + >>> import os, sys + >>> old_path = sys.path[:] + >>> sys.path.insert(0, config.VAR_DIR) + + >>> with open(os.path.join(config.VAR_DIR, 'showme.py'), 'w') as fp: + ... print >> fp, """\ + ... def showme(mailing_list): + ... print "The list's name is", mailing_list.fqdn_listname + ... + ... def realname(mailing_list): + ... print "The list's real name is", mailing_list.real_name + ... """ + +If the name of the function is the same as the module, then you only need to +name the function once. + + >>> mlist = create_list('aardvark@example.com') + >>> args.details = False + >>> args.run = 'showme' + >>> args.listname = 'aardvark@example.com' + >>> command.process(args) + The list's name is aardvark@example.com + +The function's name can also be different than the modules name. In that +case, just give the full module path name to the function you want to call. + + >>> args.run = 'showme.realname' + >>> command.process(args) + The list's real name is Aardvark + + +Multiple lists +============== + +You can run a command over more than one list by using a regular expression in +the `listname` argument. To indicate a regular expression is used, the string +must start with a caret. +:: + + >>> mlist_2 = create_list('badger@example.com') + >>> mlist_3 = create_list('badboys@example.com') + + >>> args.listname = '^.*example.com' + >>> command.process(args) + The list's real name is Aardvark + The list's real name is Badger + The list's real name is Badboys + + >>> args.listname = '^bad.*' + >>> command.process(args) + The list's real name is Badger + The list's real name is Badboys + + >>> args.listname = '^foo' + >>> command.process(args) + + +Error handling +============== + +You get an error if you try to run a function over a non-existent mailing +list. + + >>> args.listname = 'mystery@example.com' + >>> command.process(args) + No such list: mystery@example.com + +You also get an error if no mailing list is named. + + >>> args.listname = None + >>> command.process(args) + --run requires a mailing list name + + +Clean up +======== + + >>> sys.path = old_path diff --git a/src/mailman/commands/docs/withlist.txt b/src/mailman/commands/docs/withlist.txt deleted file mode 100644 index 7632c726a..000000000 --- a/src/mailman/commands/docs/withlist.txt +++ /dev/null @@ -1,125 +0,0 @@ -========================== -Operating on mailing lists -========================== - -The ``withlist`` command is a pretty powerful way to operate on mailing lists -from the command line. This command allows you to interact with a list at a -Python prompt, or process one or more mailing lists through custom made Python -functions. - -XXX Test the interactive operation of withlist - - -Getting detailed help -===================== - -Because ``withlist`` is so complex, you need to request detailed help. -:: - - >>> from mailman.commands.cli_withlist import Withlist - >>> command = Withlist() - - >>> class FakeArgs: - ... interactive = False - ... run = None - ... details = True - ... listname = [] - - >>> class FakeParser: - ... def error(self, message): - ... print message - >>> command.parser = FakeParser() - - >>> args = FakeArgs() - >>> command.process(args) - This script provides you with a general framework for interacting with a - mailing list. - ... - - -Running a command -================= - -By putting a Python function somewhere on your ``sys.path``, you can have -``withlist`` call that function on a given mailing list. The function takes a -single argument, the mailing list. -:: - - >>> import os, sys - >>> old_path = sys.path[:] - >>> sys.path.insert(0, config.VAR_DIR) - - >>> with open(os.path.join(config.VAR_DIR, 'showme.py'), 'w') as fp: - ... print >> fp, """\ - ... def showme(mailing_list): - ... print "The list's name is", mailing_list.fqdn_listname - ... - ... def realname(mailing_list): - ... print "The list's real name is", mailing_list.real_name - ... """ - -If the name of the function is the same as the module, then you only need to -name the function once. - - >>> mlist = create_list('aardvark@example.com') - >>> args.details = False - >>> args.run = 'showme' - >>> args.listname = 'aardvark@example.com' - >>> command.process(args) - The list's name is aardvark@example.com - -The function's name can also be different than the modules name. In that -case, just give the full module path name to the function you want to call. - - >>> args.run = 'showme.realname' - >>> command.process(args) - The list's real name is Aardvark - - -Multiple lists -============== - -You can run a command over more than one list by using a regular expression in -the `listname` argument. To indicate a regular expression is used, the string -must start with a caret. -:: - - >>> mlist_2 = create_list('badger@example.com') - >>> mlist_3 = create_list('badboys@example.com') - - >>> args.listname = '^.*example.com' - >>> command.process(args) - The list's real name is Aardvark - The list's real name is Badger - The list's real name is Badboys - - >>> args.listname = '^bad.*' - >>> command.process(args) - The list's real name is Badger - The list's real name is Badboys - - >>> args.listname = '^foo' - >>> command.process(args) - - -Error handling -============== - -You get an error if you try to run a function over a non-existent mailing -list. - - >>> args.listname = 'mystery@example.com' - >>> command.process(args) - No such list: mystery@example.com - -You also get an error if no mailing list is named. - - >>> args.listname = None - >>> command.process(args) - --run requires a mailing list name - - -Clean up -======== - - >>> sys.path = old_path diff --git a/src/mailman/core/docs/switchboard.rst b/src/mailman/core/docs/switchboard.rst new file mode 100644 index 000000000..751b1e640 --- /dev/null +++ b/src/mailman/core/docs/switchboard.rst @@ -0,0 +1,187 @@ +The switchboard +=============== + +The switchboard is subsystem that moves messages between queues. Each +instance of a switchboard is responsible for one queue directory. + + >>> msg = message_from_string("""\ + ... From: aperson@example.com + ... To: _xtest@example.com + ... + ... A test message. + ... """) + +Create a switchboard by giving its queue name and directory. + + >>> import os + >>> queue_directory = os.path.join(config.QUEUE_DIR, 'test') + >>> from mailman.core.switchboard import Switchboard + >>> switchboard = Switchboard('test', queue_directory) + >>> print switchboard.name + test + >>> switchboard.queue_directory == queue_directory + True + +Here's a helper function for ensuring things work correctly. + + >>> def check_qfiles(directory=None): + ... if directory is None: + ... directory = queue_directory + ... files = {} + ... for qfile in os.listdir(directory): + ... root, ext = os.path.splitext(qfile) + ... files[ext] = files.get(ext, 0) + 1 + ... if len(files) == 0: + ... print 'empty' + ... for ext in sorted(files): + ... print '{0}: {1}'.format(ext, files[ext]) + + +Enqueing and dequeing +--------------------- + +The message can be enqueued with metadata specified in the passed in +dictionary. + + >>> filebase = switchboard.enqueue(msg) + >>> check_qfiles() + .pck: 1 + +To read the contents of a queue file, dequeue it. + + >>> msg, msgdata = switchboard.dequeue(filebase) + >>> print msg.as_string() + From: aperson@example.com + To: _xtest@example.com + + A test message. + + >>> dump_msgdata(msgdata) + _parsemsg: False + version : 3 + >>> check_qfiles() + .bak: 1 + +To complete the dequeing process, removing all traces of the message file, +finish it (without preservation). + + >>> switchboard.finish(filebase) + >>> check_qfiles() + empty + +When enqueing a file, you can provide additional metadata keys by using +keyword arguments. + + >>> filebase = switchboard.enqueue(msg, {'foo': 1}, bar=2) + >>> msg, msgdata = switchboard.dequeue(filebase) + >>> switchboard.finish(filebase) + >>> dump_msgdata(msgdata) + _parsemsg: False + bar : 2 + foo : 1 + version : 3 + +Keyword arguments override keys from the metadata dictionary. + + >>> filebase = switchboard.enqueue(msg, {'foo': 1}, foo=2) + >>> msg, msgdata = switchboard.dequeue(filebase) + >>> switchboard.finish(filebase) + >>> dump_msgdata(msgdata) + _parsemsg: False + foo : 2 + version : 3 + + +Iterating over files +-------------------- + +There are two ways to iterate over all the files in a switchboard's queue. +Normally, queue files end in .pck (for 'pickle') and the easiest way to +iterate over just these files is to use the .files attribute. + + >>> filebase_1 = switchboard.enqueue(msg, foo=1) + >>> filebase_2 = switchboard.enqueue(msg, foo=2) + >>> filebase_3 = switchboard.enqueue(msg, foo=3) + >>> filebases = sorted((filebase_1, filebase_2, filebase_3)) + >>> sorted(switchboard.files) == filebases + True + >>> check_qfiles() + .pck: 3 + +You can also use the .get_files() method if you want to iterate over all the +file bases for some other extension. + + >>> for filebase in switchboard.get_files(): + ... msg, msgdata = switchboard.dequeue(filebase) + >>> bakfiles = sorted(switchboard.get_files('.bak')) + >>> bakfiles == filebases + True + >>> check_qfiles() + .bak: 3 + >>> for filebase in switchboard.get_files('.bak'): + ... switchboard.finish(filebase) + >>> check_qfiles() + empty + + +Recovering files +---------------- + +Calling .dequeue() without calling .finish() leaves .bak backup files in +place. These can be recovered when the switchboard is instantiated. + + >>> filebase_1 = switchboard.enqueue(msg, foo=1) + >>> filebase_2 = switchboard.enqueue(msg, foo=2) + >>> filebase_3 = switchboard.enqueue(msg, foo=3) + >>> for filebase in switchboard.files: + ... msg, msgdata = switchboard.dequeue(filebase) + ... # Don't call .finish() + >>> check_qfiles() + .bak: 3 + >>> switchboard_2 = Switchboard('test', queue_directory, recover=True) + >>> check_qfiles() + .pck: 3 + +The files can be recovered explicitly. + + >>> for filebase in switchboard.files: + ... msg, msgdata = switchboard.dequeue(filebase) + ... # Don't call .finish() + >>> check_qfiles() + .bak: 3 + >>> switchboard.recover_backup_files() + >>> check_qfiles() + .pck: 3 + +But the files will only be recovered at most three times before they are +considered defective. In order to prevent mail bombs and loops, once this +maximum is reached, the files will be preserved in the 'bad' queue. +:: + + >>> for filebase in switchboard.files: + ... msg, msgdata = switchboard.dequeue(filebase) + ... # Don't call .finish() + >>> check_qfiles() + .bak: 3 + >>> switchboard.recover_backup_files() + >>> check_qfiles() + empty + + >>> bad = config.switchboards['bad'] + >>> check_qfiles(bad.queue_directory) + .psv: 3 + + +Clean up +-------- + + >>> for file in os.listdir(bad.queue_directory): + ... os.remove(os.path.join(bad.queue_directory, file)) + >>> check_qfiles(bad.queue_directory) + empty + + +Queue slices +------------ + +XXX Add tests for queue slices. diff --git a/src/mailman/core/docs/switchboard.txt b/src/mailman/core/docs/switchboard.txt deleted file mode 100644 index 751b1e640..000000000 --- a/src/mailman/core/docs/switchboard.txt +++ /dev/null @@ -1,187 +0,0 @@ -The switchboard -=============== - -The switchboard is subsystem that moves messages between queues. Each -instance of a switchboard is responsible for one queue directory. - - >>> msg = message_from_string("""\ - ... From: aperson@example.com - ... To: _xtest@example.com - ... - ... A test message. - ... """) - -Create a switchboard by giving its queue name and directory. - - >>> import os - >>> queue_directory = os.path.join(config.QUEUE_DIR, 'test') - >>> from mailman.core.switchboard import Switchboard - >>> switchboard = Switchboard('test', queue_directory) - >>> print switchboard.name - test - >>> switchboard.queue_directory == queue_directory - True - -Here's a helper function for ensuring things work correctly. - - >>> def check_qfiles(directory=None): - ... if directory is None: - ... directory = queue_directory - ... files = {} - ... for qfile in os.listdir(directory): - ... root, ext = os.path.splitext(qfile) - ... files[ext] = files.get(ext, 0) + 1 - ... if len(files) == 0: - ... print 'empty' - ... for ext in sorted(files): - ... print '{0}: {1}'.format(ext, files[ext]) - - -Enqueing and dequeing ---------------------- - -The message can be enqueued with metadata specified in the passed in -dictionary. - - >>> filebase = switchboard.enqueue(msg) - >>> check_qfiles() - .pck: 1 - -To read the contents of a queue file, dequeue it. - - >>> msg, msgdata = switchboard.dequeue(filebase) - >>> print msg.as_string() - From: aperson@example.com - To: _xtest@example.com - - A test message. - - >>> dump_msgdata(msgdata) - _parsemsg: False - version : 3 - >>> check_qfiles() - .bak: 1 - -To complete the dequeing process, removing all traces of the message file, -finish it (without preservation). - - >>> switchboard.finish(filebase) - >>> check_qfiles() - empty - -When enqueing a file, you can provide additional metadata keys by using -keyword arguments. - - >>> filebase = switchboard.enqueue(msg, {'foo': 1}, bar=2) - >>> msg, msgdata = switchboard.dequeue(filebase) - >>> switchboard.finish(filebase) - >>> dump_msgdata(msgdata) - _parsemsg: False - bar : 2 - foo : 1 - version : 3 - -Keyword arguments override keys from the metadata dictionary. - - >>> filebase = switchboard.enqueue(msg, {'foo': 1}, foo=2) - >>> msg, msgdata = switchboard.dequeue(filebase) - >>> switchboard.finish(filebase) - >>> dump_msgdata(msgdata) - _parsemsg: False - foo : 2 - version : 3 - - -Iterating over files --------------------- - -There are two ways to iterate over all the files in a switchboard's queue. -Normally, queue files end in .pck (for 'pickle') and the easiest way to -iterate over just these files is to use the .files attribute. - - >>> filebase_1 = switchboard.enqueue(msg, foo=1) - >>> filebase_2 = switchboard.enqueue(msg, foo=2) - >>> filebase_3 = switchboard.enqueue(msg, foo=3) - >>> filebases = sorted((filebase_1, filebase_2, filebase_3)) - >>> sorted(switchboard.files) == filebases - True - >>> check_qfiles() - .pck: 3 - -You can also use the .get_files() method if you want to iterate over all the -file bases for some other extension. - - >>> for filebase in switchboard.get_files(): - ... msg, msgdata = switchboard.dequeue(filebase) - >>> bakfiles = sorted(switchboard.get_files('.bak')) - >>> bakfiles == filebases - True - >>> check_qfiles() - .bak: 3 - >>> for filebase in switchboard.get_files('.bak'): - ... switchboard.finish(filebase) - >>> check_qfiles() - empty - - -Recovering files ----------------- - -Calling .dequeue() without calling .finish() leaves .bak backup files in -place. These can be recovered when the switchboard is instantiated. - - >>> filebase_1 = switchboard.enqueue(msg, foo=1) - >>> filebase_2 = switchboard.enqueue(msg, foo=2) - >>> filebase_3 = switchboard.enqueue(msg, foo=3) - >>> for filebase in switchboard.files: - ... msg, msgdata = switchboard.dequeue(filebase) - ... # Don't call .finish() - >>> check_qfiles() - .bak: 3 - >>> switchboard_2 = Switchboard('test', queue_directory, recover=True) - >>> check_qfiles() - .pck: 3 - -The files can be recovered explicitly. - - >>> for filebase in switchboard.files: - ... msg, msgdata = switchboard.dequeue(filebase) - ... # Don't call .finish() - >>> check_qfiles() - .bak: 3 - >>> switchboard.recover_backup_files() - >>> check_qfiles() - .pck: 3 - -But the files will only be recovered at most three times before they are -considered defective. In order to prevent mail bombs and loops, once this -maximum is reached, the files will be preserved in the 'bad' queue. -:: - - >>> for filebase in switchboard.files: - ... msg, msgdata = switchboard.dequeue(filebase) - ... # Don't call .finish() - >>> check_qfiles() - .bak: 3 - >>> switchboard.recover_backup_files() - >>> check_qfiles() - empty - - >>> bad = config.switchboards['bad'] - >>> check_qfiles(bad.queue_directory) - .psv: 3 - - -Clean up --------- - - >>> for file in os.listdir(bad.queue_directory): - ... os.remove(os.path.join(bad.queue_directory, file)) - >>> check_qfiles(bad.queue_directory) - empty - - -Queue slices ------------- - -XXX Add tests for queue slices. diff --git a/src/mailman/docs/ACKNOWLEDGMENTS.rst b/src/mailman/docs/ACKNOWLEDGMENTS.rst new file mode 100644 index 000000000..bb971a91d --- /dev/null +++ b/src/mailman/docs/ACKNOWLEDGMENTS.rst @@ -0,0 +1,268 @@ +.. -*- coding: utf-8 -*- + +=========================== +GNU Mailman Acknowledgments +=========================== + +Copyright (C) 1998-2011 by the Free Software Foundation, Inc. + + +Core Developers +=============== + +The following folks are or have been core developers of Mailman (in reverse +alphabetical order): + +* Barry Warsaw, Mailman's yappy guard dog +* Mark Sapiro, Mailman's compulsive responder +* Tokio Kikuchi, Mailman's weatherman +* John Viega, Mailman's inventor +* Thomas Wouters, Mailman's Dutch treat +* Harald Meland, Norse Mailman +* Ken Manheimer, Mailman's savior +* Scott Cotton, Cookie-Monster + + +Steering Committee +================== + +The Mailman Steering Committee can be contacted directly via +mailman-cabal@python.org + + +Copyright Assignees +=================== + +Here is the list of other contributors who have donated large bits of +code, and have assigned copyright for contributions to the FSF: + +* Juan Carlos Rey Anaya +* Richard Barrett +* Stephan Berndts +* Norbert Bollow +* Ben Gertzfield +* Victoriano Giralt +* Mads Kiilerich +* The Dragon De Monsyne +* Les Niles +* Terri Oda +* Simone Piunno + + +Other Thanks +============ + +Thanks also go to the following people for their important contributions in +other aspects of the Mailman project: + +* Brad Knowles +* JC Dill +* Clytie Siddall + +Thanks also to Dragon for his winning Mailman logo contribution, and to Terri +Oda for the neat shortcut icon and the member documentation. + +Control.com sponsored development of several Mailman 2.1 features, including +topics filters, external membership sources, and initial virtual mailing list +support. My thanks especially to Dan Pierson and Ken Crater from Control.com. + +Here is the list of other people who have contributed useful ideas, +suggestions, bug fixes, testing, etc., or who have been very helpful in +answering questions on mailman-users. Please let me know if anybody's been +left off the list! + +* David Abrahams +* William Ahern +* Terry Allen +* Jose Paulo Moitinho de Almeida +* Sven Anderson +* Matthias Andree +* Anton Antonov +* Mike Avery +* Stonewall Ballard +* Moreno Baricevic +* Jimmy Bergman +* Jeff Berliner +* Stuart Bishop +* David Blomquist +* Bojan +* Søren Bondrup +* Grant Bowman +* Alessio Bragadini +* J\. D\. Bronson +* Stan Bubrouski +* Daniel Buchmann +* Ben Burnett +* Ted Cabeen +* Mentor Cana +* John Carnes +* Julio A. Cartaya +* Claudio Cattazzo +* Donn Cave +* David Champion +* Hye-Shik Chang +* Eric D. Christensen +* Tom G. Christensen +* Paul Cox +* Stefaniu Criste +* Robert Daeley +* Ned Dawes +* Emilio Delgado +* John Dennis +* Stefan Divjak +* Maximillian Dornseif +* Fred Drake +* Maxim Dzumanenko +* Piarres Beobide Egaña +* Rob Ellis +* Kerem Erkan +* Fil +* Patrick Finnerty +* Bob Fleck +* Erik Forsberg +* Darrell Fuhriman +* Robert Garrigós +* Carson Gaspar +* Pascal GEORGE +* Vadim Getmanshchuk +* David Gibbs +* Dmitri I GOULIAEV +* Terry Grace +* Federico Grau +* Pekka Haavisto +* David Habben +* Stig Hackvan +* Jeff Hahn +* Terry Hardie +* Paul Hebble +* Tollef Fog Heen +* Peer Heinlein +* James Henstridge +* Walter Hop +* Bert Hubert +* Henny Huisman +* Jeremy Hylton +* Ikeda Soji +* Rostyk Ivantsiv +* Ron Jarrell +* Matthias Juchem +* Tamito KAJIYAMA +* Nino Katic +* SHIGENO Kazutaka +* Ashley M. Kirchner +* Matthias Klose +* Harald Koch +* Patrick Koetter +* Eddie Kohler +* Chris Kolar +* Uros Kositer +* Andrew Kuchling +* Ricardo Kustner +* L'homme Moderne +* Sylvain Langlade +* Ed Lau +* J C Lawrence +* Greg Lindahl +* Christopher P. Lindsey +* Martin von Loewis +* Dario Lopez-Kästen +* Tanner Lovelace +* Jay Luker +* Gergely Madarasz +* Luca Maranzano +* John A. Martin +* Andrew Martynov +* Jason R. Mastaler +* Michael Mclay +* Michael Meltzer +* Marc MERLIN +* Nigel Metheringham +* Dan Mick +* Garey Mills +* Martin Mokrejs +* Michael Fischer v. Mollard +* David Martínez Moreno +* Dirk Mueller +* Jonas Muerer +* Erik Myllymaki +* Balazs Nagy +* Moritz Naumann +* Dale Newfield +* Hrvoje Niksic +* Les Niles +* Mike Noyes +* David B. O'Donnell +* Timothy O'Malley +* "office" +* Dan Ohnesorg +* Gerald Oskoboiny +* Eva Österlind +* Toni Panadès +* Jon Parise +* Chris Pepper +* Tim Peters +* Joe Peterson +* PieterB +* Rodolfo Pilas +* Skye Poier +* Martin Pool +* Don Porter +* Francesco Potortì +* Bob Puff +* Michael Ranner +* John Read +* Sean Reifschneider +* Christian Reis +* Ademar de Souza Reis, Jr. +* Bernhard Reiter +* Stephan Richter +* Tristan Roddis +* Heiko Rommel +* Luigi Rosa +* Guido van Rossum +* Nicholas Russo +* Chris Ryan +* Cabel Sasser +* Bartosz Sawicki +* Kai Schaetzl +* Karoly Segesdi +* Gleydson Mazioli da Silva +* Pasi Sjöholm +* Chris Snell +* Mikhail Sobolev +* Greg Stein +* Dale Stimson +* Students of HIT +* Szabolcs Szigeti +* Vizi Szilard +* David T-G +* Owen Taylor +* Danny Terweij +* Jim Tittsler +* Todd (Freedom Lover) +* Roger Tsang +* Chuq Von Rospach +* Jens Vagelpohl +* Valia V. Vaneeva +* Anti Veeranna +* Todd Vierling +* Bill Wagner +* Greg Ward +* Mark Weaver +* Kathleen Webb +* Florian Weimer +* Ousmane Wilane +* Dan Wilder +* Seb Wills +* Dai Xiaoguang +* Ping Yeh +* YASUDA Yukihiro +* Michael Yount +* Blair Zajac +* Mikhail Zabaluev +* Noam Zeilberger +* Daniel Zeiss +* Todd Zullinger + +And everyone else on mailman-developers@python.org and +mailman-users@python.org! Thank you, all. diff --git a/src/mailman/docs/ACKNOWLEDGMENTS.txt b/src/mailman/docs/ACKNOWLEDGMENTS.txt deleted file mode 100644 index bb971a91d..000000000 --- a/src/mailman/docs/ACKNOWLEDGMENTS.txt +++ /dev/null @@ -1,268 +0,0 @@ -.. -*- coding: utf-8 -*- - -=========================== -GNU Mailman Acknowledgments -=========================== - -Copyright (C) 1998-2011 by the Free Software Foundation, Inc. - - -Core Developers -=============== - -The following folks are or have been core developers of Mailman (in reverse -alphabetical order): - -* Barry Warsaw, Mailman's yappy guard dog -* Mark Sapiro, Mailman's compulsive responder -* Tokio Kikuchi, Mailman's weatherman -* John Viega, Mailman's inventor -* Thomas Wouters, Mailman's Dutch treat -* Harald Meland, Norse Mailman -* Ken Manheimer, Mailman's savior -* Scott Cotton, Cookie-Monster - - -Steering Committee -================== - -The Mailman Steering Committee can be contacted directly via -mailman-cabal@python.org - - -Copyright Assignees -=================== - -Here is the list of other contributors who have donated large bits of -code, and have assigned copyright for contributions to the FSF: - -* Juan Carlos Rey Anaya -* Richard Barrett -* Stephan Berndts -* Norbert Bollow -* Ben Gertzfield -* Victoriano Giralt -* Mads Kiilerich -* The Dragon De Monsyne -* Les Niles -* Terri Oda -* Simone Piunno - - -Other Thanks -============ - -Thanks also go to the following people for their important contributions in -other aspects of the Mailman project: - -* Brad Knowles -* JC Dill -* Clytie Siddall - -Thanks also to Dragon for his winning Mailman logo contribution, and to Terri -Oda for the neat shortcut icon and the member documentation. - -Control.com sponsored development of several Mailman 2.1 features, including -topics filters, external membership sources, and initial virtual mailing list -support. My thanks especially to Dan Pierson and Ken Crater from Control.com. - -Here is the list of other people who have contributed useful ideas, -suggestions, bug fixes, testing, etc., or who have been very helpful in -answering questions on mailman-users. Please let me know if anybody's been -left off the list! - -* David Abrahams -* William Ahern -* Terry Allen -* Jose Paulo Moitinho de Almeida -* Sven Anderson -* Matthias Andree -* Anton Antonov -* Mike Avery -* Stonewall Ballard -* Moreno Baricevic -* Jimmy Bergman -* Jeff Berliner -* Stuart Bishop -* David Blomquist -* Bojan -* Søren Bondrup -* Grant Bowman -* Alessio Bragadini -* J\. D\. Bronson -* Stan Bubrouski -* Daniel Buchmann -* Ben Burnett -* Ted Cabeen -* Mentor Cana -* John Carnes -* Julio A. Cartaya -* Claudio Cattazzo -* Donn Cave -* David Champion -* Hye-Shik Chang -* Eric D. Christensen -* Tom G. Christensen -* Paul Cox -* Stefaniu Criste -* Robert Daeley -* Ned Dawes -* Emilio Delgado -* John Dennis -* Stefan Divjak -* Maximillian Dornseif -* Fred Drake -* Maxim Dzumanenko -* Piarres Beobide Egaña -* Rob Ellis -* Kerem Erkan -* Fil -* Patrick Finnerty -* Bob Fleck -* Erik Forsberg -* Darrell Fuhriman -* Robert Garrigós -* Carson Gaspar -* Pascal GEORGE -* Vadim Getmanshchuk -* David Gibbs -* Dmitri I GOULIAEV -* Terry Grace -* Federico Grau -* Pekka Haavisto -* David Habben -* Stig Hackvan -* Jeff Hahn -* Terry Hardie -* Paul Hebble -* Tollef Fog Heen -* Peer Heinlein -* James Henstridge -* Walter Hop -* Bert Hubert -* Henny Huisman -* Jeremy Hylton -* Ikeda Soji -* Rostyk Ivantsiv -* Ron Jarrell -* Matthias Juchem -* Tamito KAJIYAMA -* Nino Katic -* SHIGENO Kazutaka -* Ashley M. Kirchner -* Matthias Klose -* Harald Koch -* Patrick Koetter -* Eddie Kohler -* Chris Kolar -* Uros Kositer -* Andrew Kuchling -* Ricardo Kustner -* L'homme Moderne -* Sylvain Langlade -* Ed Lau -* J C Lawrence -* Greg Lindahl -* Christopher P. Lindsey -* Martin von Loewis -* Dario Lopez-Kästen -* Tanner Lovelace -* Jay Luker -* Gergely Madarasz -* Luca Maranzano -* John A. Martin -* Andrew Martynov -* Jason R. Mastaler -* Michael Mclay -* Michael Meltzer -* Marc MERLIN -* Nigel Metheringham -* Dan Mick -* Garey Mills -* Martin Mokrejs -* Michael Fischer v. Mollard -* David Martínez Moreno -* Dirk Mueller -* Jonas Muerer -* Erik Myllymaki -* Balazs Nagy -* Moritz Naumann -* Dale Newfield -* Hrvoje Niksic -* Les Niles -* Mike Noyes -* David B. O'Donnell -* Timothy O'Malley -* "office" -* Dan Ohnesorg -* Gerald Oskoboiny -* Eva Österlind -* Toni Panadès -* Jon Parise -* Chris Pepper -* Tim Peters -* Joe Peterson -* PieterB -* Rodolfo Pilas -* Skye Poier -* Martin Pool -* Don Porter -* Francesco Potortì -* Bob Puff -* Michael Ranner -* John Read -* Sean Reifschneider -* Christian Reis -* Ademar de Souza Reis, Jr. -* Bernhard Reiter -* Stephan Richter -* Tristan Roddis -* Heiko Rommel -* Luigi Rosa -* Guido van Rossum -* Nicholas Russo -* Chris Ryan -* Cabel Sasser -* Bartosz Sawicki -* Kai Schaetzl -* Karoly Segesdi -* Gleydson Mazioli da Silva -* Pasi Sjöholm -* Chris Snell -* Mikhail Sobolev -* Greg Stein -* Dale Stimson -* Students of HIT -* Szabolcs Szigeti -* Vizi Szilard -* David T-G -* Owen Taylor -* Danny Terweij -* Jim Tittsler -* Todd (Freedom Lover) -* Roger Tsang -* Chuq Von Rospach -* Jens Vagelpohl -* Valia V. Vaneeva -* Anti Veeranna -* Todd Vierling -* Bill Wagner -* Greg Ward -* Mark Weaver -* Kathleen Webb -* Florian Weimer -* Ousmane Wilane -* Dan Wilder -* Seb Wills -* Dai Xiaoguang -* Ping Yeh -* YASUDA Yukihiro -* Michael Yount -* Blair Zajac -* Mikhail Zabaluev -* Noam Zeilberger -* Daniel Zeiss -* Todd Zullinger - -And everyone else on mailman-developers@python.org and -mailman-users@python.org! Thank you, all. diff --git a/src/mailman/docs/MTA.rst b/src/mailman/docs/MTA.rst new file mode 100644 index 000000000..f541d3838 --- /dev/null +++ b/src/mailman/docs/MTA.rst @@ -0,0 +1,129 @@ +=========================== +Hooking up your mail server +=========================== + +Mailman needs to be hooked up to your mail server both to accept incoming mail +and to deliver outgoing mail. Mailman itself never delivers messages to the +end user; it lets its immediate upstream mail server do that. + +The preferred way to allow Mailman to accept incoming messages from your mail +server is to use the `Local Mail Transfer Protocol`_ (LMTP_) interface. Most +open source mail server support LMTP for local delivery, and this is much more +efficient than spawning a process just to do the delivery. + +Your mail server should also accept `Simple Mail Transfer Protocol`_ (SMTP_) +connections from Mailman, for all outgoing messages. + +The specific instructions for hooking your mail server up to Mailman differs +depending on which mail server you're using. The following are instructions +for the popular open source mail servers. + +Note that Mailman provides lots of configuration variables that you can use to +tweak performance for your operating environment. See the +`src/mailman/config/schema.cfg` file for details. + + +Exim +==== + +Contributions are welcome! + + +Postfix +======= + +Mailman settings +---------------- + +You need to tell Mailman that you are using the Postfix mail server. In your +`mailman.cfg` file, add the following section:: + + [mta] + incoming: mailman.mta.postfix.LMTP + outgoing: mailman.mta.deliver.deliver + lmtp_host: mail.example.com + lmtp_port: 8024 + smtp_host: mail.example.com + smtp_port: 25 + +Some of these settings are already the default, so take a look at Mailman's +`src/mailman/config/schema.cfg` file for details. You'll need to change the +`lmtp_host` and `smtp_host` to the appropriate host names of course. +Generally, Postfix will listen for incoming SMTP connections on port 25. +Postfix will deliver via LMTP over port 24 by default, however if you are not +running Mailman as root, you'll need to change this to a higher port number, +as shown above. + + +Basic Postfix connections +------------------------- + +There are several ways to hook Postfix_ up to Mailman, so here are the +simplest instructions. The following settings should be added to Postfix's +`main.cf` file. + +Mailman supports a technique called `Variable Envelope Return Path`_ (VERP) to +disambiguate and accurately record bounces. By default Mailman's VERP +delimiter is the `+` sign, so adding this setting allows Postfix to properly +handle Mailman's VERP'd messages:: + + # Support the default VERP delimiter. + recipient_delimiter = + + +In older versions of Postfix, unknown local recipients generated a temporary +failure. It's much better (and the default in newer Postfix releases) to +treat them as permanent failures. You can add this to your `main.cf` file if +needed (use the `postconf`_ to check the defaults):: + + unknown_local_recipient_reject_code = 550 + +While generally not necessary if you set `recipient_delimiter` as described +above, it's better for Postfix to not treat `owner-` and `-request` addresses +specially:: + + owner_request_special = no + + +Transport maps +-------------- + +By default, Mailman works well with Postfix transport maps as a way to deliver +incoming messages to Mailman's LMTP server. Mailman will automatically write +the correct transport map when its `bin/mailman genaliases` command is run, or +whenever a mailing list is created or removed via other commands. To connect +Postfix to Mailman's LMTP server, add the following to Postfix's `main.cf` +file:: + + transport_maps = + hash:/path-to-mailman/var/data/postfix_lmtp + local_recipient_maps = + hash:/path-to-mailman/var/data/postfix_lmtp + +where `path-to-mailman` is replaced with the actual path that you're running +Mailman from. Setting `local_recipient_maps` as well as `transport_maps` +allows Postfix to properly reject all messages destined for non-existent local +users. + + +Virtual domains +--------------- + +TBD: figure out how virtual domains interact with the transport maps. + + +Sendmail +======== + +Contributions are welcome! + + +.. _`Local Mail Transfer Protocol`: + http://en.wikipedia.org/wiki/Local_Mail_Transfer_Protocol +.. _LMTP: http://www.faqs.org/rfcs/rfc2033.html +.. _`Simple Mail Transfer Protocol`: + http://en.wikipedia.org/wiki/Simple_Mail_Transfer_Protocol +.. _SMTP: http://www.faqs.org/rfcs/rfc5321.html +.. _Postfix: http://www.postfix.org +.. _`Variable Envelope Return Path`: + http://en.wikipedia.org/wiki/Variable_envelope_return_path +.. _postconf: http://www.postfix.org/postconf.1.html diff --git a/src/mailman/docs/MTA.txt b/src/mailman/docs/MTA.txt deleted file mode 100644 index f541d3838..000000000 --- a/src/mailman/docs/MTA.txt +++ /dev/null @@ -1,129 +0,0 @@ -=========================== -Hooking up your mail server -=========================== - -Mailman needs to be hooked up to your mail server both to accept incoming mail -and to deliver outgoing mail. Mailman itself never delivers messages to the -end user; it lets its immediate upstream mail server do that. - -The preferred way to allow Mailman to accept incoming messages from your mail -server is to use the `Local Mail Transfer Protocol`_ (LMTP_) interface. Most -open source mail server support LMTP for local delivery, and this is much more -efficient than spawning a process just to do the delivery. - -Your mail server should also accept `Simple Mail Transfer Protocol`_ (SMTP_) -connections from Mailman, for all outgoing messages. - -The specific instructions for hooking your mail server up to Mailman differs -depending on which mail server you're using. The following are instructions -for the popular open source mail servers. - -Note that Mailman provides lots of configuration variables that you can use to -tweak performance for your operating environment. See the -`src/mailman/config/schema.cfg` file for details. - - -Exim -==== - -Contributions are welcome! - - -Postfix -======= - -Mailman settings ----------------- - -You need to tell Mailman that you are using the Postfix mail server. In your -`mailman.cfg` file, add the following section:: - - [mta] - incoming: mailman.mta.postfix.LMTP - outgoing: mailman.mta.deliver.deliver - lmtp_host: mail.example.com - lmtp_port: 8024 - smtp_host: mail.example.com - smtp_port: 25 - -Some of these settings are already the default, so take a look at Mailman's -`src/mailman/config/schema.cfg` file for details. You'll need to change the -`lmtp_host` and `smtp_host` to the appropriate host names of course. -Generally, Postfix will listen for incoming SMTP connections on port 25. -Postfix will deliver via LMTP over port 24 by default, however if you are not -running Mailman as root, you'll need to change this to a higher port number, -as shown above. - - -Basic Postfix connections -------------------------- - -There are several ways to hook Postfix_ up to Mailman, so here are the -simplest instructions. The following settings should be added to Postfix's -`main.cf` file. - -Mailman supports a technique called `Variable Envelope Return Path`_ (VERP) to -disambiguate and accurately record bounces. By default Mailman's VERP -delimiter is the `+` sign, so adding this setting allows Postfix to properly -handle Mailman's VERP'd messages:: - - # Support the default VERP delimiter. - recipient_delimiter = + - -In older versions of Postfix, unknown local recipients generated a temporary -failure. It's much better (and the default in newer Postfix releases) to -treat them as permanent failures. You can add this to your `main.cf` file if -needed (use the `postconf`_ to check the defaults):: - - unknown_local_recipient_reject_code = 550 - -While generally not necessary if you set `recipient_delimiter` as described -above, it's better for Postfix to not treat `owner-` and `-request` addresses -specially:: - - owner_request_special = no - - -Transport maps --------------- - -By default, Mailman works well with Postfix transport maps as a way to deliver -incoming messages to Mailman's LMTP server. Mailman will automatically write -the correct transport map when its `bin/mailman genaliases` command is run, or -whenever a mailing list is created or removed via other commands. To connect -Postfix to Mailman's LMTP server, add the following to Postfix's `main.cf` -file:: - - transport_maps = - hash:/path-to-mailman/var/data/postfix_lmtp - local_recipient_maps = - hash:/path-to-mailman/var/data/postfix_lmtp - -where `path-to-mailman` is replaced with the actual path that you're running -Mailman from. Setting `local_recipient_maps` as well as `transport_maps` -allows Postfix to properly reject all messages destined for non-existent local -users. - - -Virtual domains ---------------- - -TBD: figure out how virtual domains interact with the transport maps. - - -Sendmail -======== - -Contributions are welcome! - - -.. _`Local Mail Transfer Protocol`: - http://en.wikipedia.org/wiki/Local_Mail_Transfer_Protocol -.. _LMTP: http://www.faqs.org/rfcs/rfc2033.html -.. _`Simple Mail Transfer Protocol`: - http://en.wikipedia.org/wiki/Simple_Mail_Transfer_Protocol -.. _SMTP: http://www.faqs.org/rfcs/rfc5321.html -.. _Postfix: http://www.postfix.org -.. _`Variable Envelope Return Path`: - http://en.wikipedia.org/wiki/Variable_envelope_return_path -.. _postconf: http://www.postfix.org/postconf.1.html diff --git a/src/mailman/docs/README.rst b/src/mailman/docs/README.rst new file mode 100644 index 000000000..b7a247de4 --- /dev/null +++ b/src/mailman/docs/README.rst @@ -0,0 +1,117 @@ +================================================ +Mailman - The GNU Mailing List Management System +================================================ + +This is `GNU Mailman`_, a mailing list management system distributed under the +terms of the `GNU General Public License`_ (GPL) version 3 or later. + +Mailman is written in Python_, a free object-oriented programming language. +Python is available for all platforms that Mailman is supported on, which +includes GNU/Linux and most other Unix-like operating systems (e.g. Solaris, +\*BSD, MacOSX, etc.). Mailman is not supported on Windows, although web and +mail clients on any platform should be able to interact with Mailman just +fine. + +Learn more about GNU Mailman in the `Getting Started`_ documentation. + + +Copyright +========= + +Copyright 1998-2011 by the Free Software Foundation, Inc. + +This file is part of GNU Mailman. + +GNU Mailman is free software: you can redistribute it and/or modify it under +the terms of the GNU General Public License as published by the Free Software +Foundation, either version 3 of the License, or (at your option) any later +version. + +GNU Mailman is distributed in the hope that it will be useful, but WITHOUT ANY +WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR +A PARTICULAR PURPOSE. See the GNU General Public License for more details. + +You should have received a copy of the GNU General Public License along with +GNU Mailman. If not, see . + + +Spelling +======== + +The name of this software is spelled `Mailman` with a leading capital `M` +but with a lower case second `m`. Any other spelling is incorrect. Its full +name is `GNU Mailman` but is often referred colloquially as `Mailman`. + + +History +======= + +Mailman was originally developed by John Viega. Subsequent development +(through version 1.0b3) was by Ken Manheimer. Further work towards the 1.0 +final release was a group effort, with the core contributors being: Barry +Warsaw, Ken Manheimer, Scott Cotton, Harald Meland, and John Viega. Version +1.0 and beyond have been primarily maintained by Barry Warsaw with +contributions from many; see the ACKNOWLEDGMENTS file for details. Jeremy +Hylton helped considerably with the Pipermail code in Mailman 2.0. Mailman +2.1 is primarily maintained by Mark Sapiro and Tokio Kikuchi. Barry Warsaw is +the lead developer on Mailman 3. + + +Help +==== + +The Mailman home page is: + + http://www.list.org + +with mirrors at: + + * http://www.gnu.org/software/mailman + * http://mailman.sf.net + +The community driven wiki (including the FAQ_) is at: + + http://wiki.list.org + +Other help resources, such as on-line documentation, links to the mailing +lists and archives, etc., are available at: + + http://www.list.org/help.html + + +Requirements +============ + +Mailman 3.0 requires `Python 2.6`_ or newer. + + +.. _`GNU Mailman`: http://www.list.org +.. _`GNU General Public License`: http://www.gnu.org/licenses/gpl.txt +.. _`Getting Started`: START.html +.. _Python: http://www.python.org +.. _FAQ: http://wiki.list.org/display/DOC/Frequently+Asked+Questions +.. _`Python 2.6`: http://www.python.org/download/releases/2.6.6/ + + +Table of Contents +================= + +.. toctree:: + :glob: + + START + MTA + NEWS + STYLEGUIDE + ACKNOWLEDGMENTS + ../bin/docs/* + ../commands/docs/* + ../model/docs/* + ../app/docs/* + ../pipeline/docs/* + ../queue/docs/* + ../rest/docs/* + ../chains/docs/* + ../rules/docs/* + ../archiving/docs/* + ../mta/docs/* diff --git a/src/mailman/docs/README.txt b/src/mailman/docs/README.txt deleted file mode 100644 index b7a247de4..000000000 --- a/src/mailman/docs/README.txt +++ /dev/null @@ -1,117 +0,0 @@ -================================================ -Mailman - The GNU Mailing List Management System -================================================ - -This is `GNU Mailman`_, a mailing list management system distributed under the -terms of the `GNU General Public License`_ (GPL) version 3 or later. - -Mailman is written in Python_, a free object-oriented programming language. -Python is available for all platforms that Mailman is supported on, which -includes GNU/Linux and most other Unix-like operating systems (e.g. Solaris, -\*BSD, MacOSX, etc.). Mailman is not supported on Windows, although web and -mail clients on any platform should be able to interact with Mailman just -fine. - -Learn more about GNU Mailman in the `Getting Started`_ documentation. - - -Copyright -========= - -Copyright 1998-2011 by the Free Software Foundation, Inc. - -This file is part of GNU Mailman. - -GNU Mailman is free software: you can redistribute it and/or modify it under -the terms of the GNU General Public License as published by the Free Software -Foundation, either version 3 of the License, or (at your option) any later -version. - -GNU Mailman is distributed in the hope that it will be useful, but WITHOUT ANY -WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR -A PARTICULAR PURPOSE. See the GNU General Public License for more details. - -You should have received a copy of the GNU General Public License along with -GNU Mailman. If not, see . - - -Spelling -======== - -The name of this software is spelled `Mailman` with a leading capital `M` -but with a lower case second `m`. Any other spelling is incorrect. Its full -name is `GNU Mailman` but is often referred colloquially as `Mailman`. - - -History -======= - -Mailman was originally developed by John Viega. Subsequent development -(through version 1.0b3) was by Ken Manheimer. Further work towards the 1.0 -final release was a group effort, with the core contributors being: Barry -Warsaw, Ken Manheimer, Scott Cotton, Harald Meland, and John Viega. Version -1.0 and beyond have been primarily maintained by Barry Warsaw with -contributions from many; see the ACKNOWLEDGMENTS file for details. Jeremy -Hylton helped considerably with the Pipermail code in Mailman 2.0. Mailman -2.1 is primarily maintained by Mark Sapiro and Tokio Kikuchi. Barry Warsaw is -the lead developer on Mailman 3. - - -Help -==== - -The Mailman home page is: - - http://www.list.org - -with mirrors at: - - * http://www.gnu.org/software/mailman - * http://mailman.sf.net - -The community driven wiki (including the FAQ_) is at: - - http://wiki.list.org - -Other help resources, such as on-line documentation, links to the mailing -lists and archives, etc., are available at: - - http://www.list.org/help.html - - -Requirements -============ - -Mailman 3.0 requires `Python 2.6`_ or newer. - - -.. _`GNU Mailman`: http://www.list.org -.. _`GNU General Public License`: http://www.gnu.org/licenses/gpl.txt -.. _`Getting Started`: START.html -.. _Python: http://www.python.org -.. _FAQ: http://wiki.list.org/display/DOC/Frequently+Asked+Questions -.. _`Python 2.6`: http://www.python.org/download/releases/2.6.6/ - - -Table of Contents -================= - -.. toctree:: - :glob: - - START - MTA - NEWS - STYLEGUIDE - ACKNOWLEDGMENTS - ../bin/docs/* - ../commands/docs/* - ../model/docs/* - ../app/docs/* - ../pipeline/docs/* - ../queue/docs/* - ../rest/docs/* - ../chains/docs/* - ../rules/docs/* - ../archiving/docs/* - ../mta/docs/* diff --git a/src/mailman/docs/STYLEGUIDE.rst b/src/mailman/docs/STYLEGUIDE.rst new file mode 100644 index 000000000..32b2da72f --- /dev/null +++ b/src/mailman/docs/STYLEGUIDE.rst @@ -0,0 +1,125 @@ +============================== +GNU Mailman Coding Style Guide +============================== + +Copyright (C) 2002-2011 Barry A. Warsaw + + +Python coding style guide for GNU Mailman +========================================= + +*NOTE*: The canonical version of this style guide can be found at: + +http://barry.warsaw.us/software/STYLEGUIDE.txt + +This document contains a style guide for Python programming, as used in GNU +Mailman. `PEP 8`_ is the basis for this style guide so it's recommendations +should be followed except for the differences outlined here. This document +assumes the use of Python 2.6 or 2.7, but not (yet) Python 3. + +* After file comments (e.g. license block), add a ``__metaclass__`` definition + so that all classes will be new-style. Following that, add an ``__all__`` + section that names, one-per-line, all the public names exported by this + module. You should enable absolute imports and unicode literals. See the + `GNU Mailman Python template`_ as an example. + +* Imports are always put at the top of the file, just after any module + comments and docstrings, and before module globals and constants, but after + any ``__future__`` imports, or ``__metaclass__`` and ``__all__`` + definitions. + + Imports should be grouped, with the order being: + + 1. non-from imports for standard and third party libraries + 2. non-from imports from the application + 3. from-imports from the standard and third party libraries + 4. from-imports from the application + + From-imports should follow non-from imports. Dotted imports should follow + non-dotted imports. Non-dotted imports should be grouped by increasing + length, while dotted imports should be grouped alphabetically. + +* In general, there should be one class per module. Keep files small, but + it's okay to group related code together. List everything exported from the + module in the ``__all__``. + +* Right hanging comments are discouraged, in favor of preceding comments. + E.g. bad:: + + foo = blarzigop(bar) # if you don't blarzigop it, it'll shlorp + + Good:: + + # If you don't blarzigop it, it'll shlorp. + foo = blarzigop(bar) + + Comments should always be complete sentences, with proper capitalization and + full stops at the end. + +* Major sections of code in a module should be separated by form feed + characters (e.g. ``^L`` -- that's a single character control-L not two + characters). This helps with Emacs navigation. + + Put a ``^L`` before module-level functions, before class definitions, before + big blocks of constants which follow imports, and any place else that would + be convenient to jump to. Always put two blank lines before a ``^L``. + +* Put two blank lines between any top level construct or block of code + (e.g. after import blocks). Put only one blank line between methods in a + class. No blank lines between the class definition and the first method in + the class. No blank lines between a class/method and its docstrings. + +* Try to minimize the vertical whitespace in a class or function. If you're + inclined to separate stanzas of code for readability, consider putting a + comment in describing what the next stanza's purpose is. Don't put stupid + or obvious comments in just to avoid vertical whitespace though. + +* Unless internal quote characters would mess things up, the general rule is + that single quotes should be used for short strings, double quotes for + triple-quoted multi-line strings and docstrings. E.g.:: + + foo = 'a foo thing' + warn = "Don't mess things up" + notice = """Our three chief weapons are: + - surprise + - deception + - an almost fanatical devotion to the pope + """ + +* Write docstrings for modules, functions, classes, and methods. Docstrings + can be omitted for special methods (e.g. __init__() or __str__()) where the + meaning is obvious. + +* PEP 257 describes good docstrings conventions. Note that most importantly, + the """ that ends a multiline docstring should be on a line by itself, e.g.:: + + """Return a foobang + + Optional plotz says to frobnicate the bizbaz first. + """ + +* For one liner docstrings, keep the closing """ on the same line. + +* ``fill-column`` for docstrings should be 78. + +* When testing the emptiness of sequences, use ``if len(seq) == 0`` instead of + relying on the falseness of empty sequences. However, if a variable can be + one of several false values, it's okay to just use ``if seq``, though a + preceding comment is usually in order. + +* Always decide whether a class's methods and instance variables should be + public or non-public. + + Single leading underscores are generally preferred for non-public + attributes. Use double leading underscores only in classes designed for + inheritance to ensure that truly private attributes will never name clash. + These should be rare. + + Public attributes should have no leading or trailing underscores unless they + conflict with reserved words, in which case, a single trailing underscore is + preferable to a leading one, or a corrupted spelling, e.g. ``class_`` rather + than ``klass``. + + +.. _`PEP 8`: http://www.python.org/peps/pep-0008.html +.. _`GNU Mailman Python template`: http://bazaar.launchpad.net/~mailman-coders/mailman/3.0/annotate/head%3A/template.py diff --git a/src/mailman/docs/STYLEGUIDE.txt b/src/mailman/docs/STYLEGUIDE.txt deleted file mode 100644 index 32b2da72f..000000000 --- a/src/mailman/docs/STYLEGUIDE.txt +++ /dev/null @@ -1,125 +0,0 @@ -============================== -GNU Mailman Coding Style Guide -============================== - -Copyright (C) 2002-2011 Barry A. Warsaw - - -Python coding style guide for GNU Mailman -========================================= - -*NOTE*: The canonical version of this style guide can be found at: - -http://barry.warsaw.us/software/STYLEGUIDE.txt - -This document contains a style guide for Python programming, as used in GNU -Mailman. `PEP 8`_ is the basis for this style guide so it's recommendations -should be followed except for the differences outlined here. This document -assumes the use of Python 2.6 or 2.7, but not (yet) Python 3. - -* After file comments (e.g. license block), add a ``__metaclass__`` definition - so that all classes will be new-style. Following that, add an ``__all__`` - section that names, one-per-line, all the public names exported by this - module. You should enable absolute imports and unicode literals. See the - `GNU Mailman Python template`_ as an example. - -* Imports are always put at the top of the file, just after any module - comments and docstrings, and before module globals and constants, but after - any ``__future__`` imports, or ``__metaclass__`` and ``__all__`` - definitions. - - Imports should be grouped, with the order being: - - 1. non-from imports for standard and third party libraries - 2. non-from imports from the application - 3. from-imports from the standard and third party libraries - 4. from-imports from the application - - From-imports should follow non-from imports. Dotted imports should follow - non-dotted imports. Non-dotted imports should be grouped by increasing - length, while dotted imports should be grouped alphabetically. - -* In general, there should be one class per module. Keep files small, but - it's okay to group related code together. List everything exported from the - module in the ``__all__``. - -* Right hanging comments are discouraged, in favor of preceding comments. - E.g. bad:: - - foo = blarzigop(bar) # if you don't blarzigop it, it'll shlorp - - Good:: - - # If you don't blarzigop it, it'll shlorp. - foo = blarzigop(bar) - - Comments should always be complete sentences, with proper capitalization and - full stops at the end. - -* Major sections of code in a module should be separated by form feed - characters (e.g. ``^L`` -- that's a single character control-L not two - characters). This helps with Emacs navigation. - - Put a ``^L`` before module-level functions, before class definitions, before - big blocks of constants which follow imports, and any place else that would - be convenient to jump to. Always put two blank lines before a ``^L``. - -* Put two blank lines between any top level construct or block of code - (e.g. after import blocks). Put only one blank line between methods in a - class. No blank lines between the class definition and the first method in - the class. No blank lines between a class/method and its docstrings. - -* Try to minimize the vertical whitespace in a class or function. If you're - inclined to separate stanzas of code for readability, consider putting a - comment in describing what the next stanza's purpose is. Don't put stupid - or obvious comments in just to avoid vertical whitespace though. - -* Unless internal quote characters would mess things up, the general rule is - that single quotes should be used for short strings, double quotes for - triple-quoted multi-line strings and docstrings. E.g.:: - - foo = 'a foo thing' - warn = "Don't mess things up" - notice = """Our three chief weapons are: - - surprise - - deception - - an almost fanatical devotion to the pope - """ - -* Write docstrings for modules, functions, classes, and methods. Docstrings - can be omitted for special methods (e.g. __init__() or __str__()) where the - meaning is obvious. - -* PEP 257 describes good docstrings conventions. Note that most importantly, - the """ that ends a multiline docstring should be on a line by itself, e.g.:: - - """Return a foobang - - Optional plotz says to frobnicate the bizbaz first. - """ - -* For one liner docstrings, keep the closing """ on the same line. - -* ``fill-column`` for docstrings should be 78. - -* When testing the emptiness of sequences, use ``if len(seq) == 0`` instead of - relying on the falseness of empty sequences. However, if a variable can be - one of several false values, it's okay to just use ``if seq``, though a - preceding comment is usually in order. - -* Always decide whether a class's methods and instance variables should be - public or non-public. - - Single leading underscores are generally preferred for non-public - attributes. Use double leading underscores only in classes designed for - inheritance to ensure that truly private attributes will never name clash. - These should be rare. - - Public attributes should have no leading or trailing underscores unless they - conflict with reserved words, in which case, a single trailing underscore is - preferable to a leading one, or a corrupted spelling, e.g. ``class_`` rather - than ``klass``. - - -.. _`PEP 8`: http://www.python.org/peps/pep-0008.html -.. _`GNU Mailman Python template`: http://bazaar.launchpad.net/~mailman-coders/mailman/3.0/annotate/head%3A/template.py diff --git a/src/mailman/model/docs/addresses.rst b/src/mailman/model/docs/addresses.rst new file mode 100644 index 000000000..01e68c954 --- /dev/null +++ b/src/mailman/model/docs/addresses.rst @@ -0,0 +1,204 @@ +=============== +Email addresses +=============== + +Addresses represent a text email address, along with some meta data about +those addresses, such as their registration date, and whether and when they've +been validated. Addresses may be linked to the users that Mailman knows +about. Addresses are subscribed to mailing lists though members. + + >>> from mailman.interfaces.usermanager import IUserManager + >>> from zope.component import getUtility + >>> user_manager = getUtility(IUserManager) + + +Creating addresses +================== + +Addresses are created directly through the user manager, which starts out with +no addresses. + + >>> dump_list(address.email for address in user_manager.addresses) + *Empty* + +Creating an unlinked email address is straightforward. + + >>> address_1 = user_manager.create_address('aperson@example.com') + >>> dump_list(address.email for address in user_manager.addresses) + aperson@example.com + +However, such addresses have no real name. + + >>> print address_1.real_name + + +You can also create an email address object with a real name. + + >>> address_2 = user_manager.create_address( + ... 'bperson@example.com', 'Ben Person') + >>> dump_list(address.email for address in user_manager.addresses) + aperson@example.com + bperson@example.com + >>> dump_list(address.real_name for address in user_manager.addresses) + + Ben Person + +The ``str()`` of the address is the RFC 2822 preferred originator format, +while the ``repr()`` carries more information. + + >>> print str(address_2) + Ben Person + >>> print repr(address_2) + [not verified] at 0x...> + +You can assign real names to existing addresses. + + >>> address_1.real_name = 'Anne Person' + >>> dump_list(address.real_name for address in user_manager.addresses) + Anne Person + Ben Person + +These addresses are not linked to users, and can be seen by searching the user +manager for an associated user. + + >>> print user_manager.get_user('aperson@example.com') + None + >>> print user_manager.get_user('bperson@example.com') + None + +You can create email addresses that are linked to users by using a different +interface. + + >>> user_1 = user_manager.create_user( + ... 'cperson@example.com', u'Claire Person') + >>> dump_list(address.email for address in user_1.addresses) + cperson@example.com + >>> dump_list(address.email for address in user_manager.addresses) + aperson@example.com + bperson@example.com + cperson@example.com + >>> dump_list(address.real_name for address in user_manager.addresses) + Anne Person + Ben Person + Claire Person + +And now you can find the associated user. + + >>> print user_manager.get_user('aperson@example.com') + None + >>> print user_manager.get_user('bperson@example.com') + None + >>> user_manager.get_user('cperson@example.com') + + + +Deleting addresses +================== + +You can remove an unlinked address from the user manager. + + >>> user_manager.delete_address(address_1) + >>> dump_list(address.email for address in user_manager.addresses) + bperson@example.com + cperson@example.com + >>> dump_list(address.real_name for address in user_manager.addresses) + Ben Person + Claire Person + +Deleting a linked address does not delete the user, but it does unlink the +address from the user. + + >>> dump_list(address.email for address in user_1.addresses) + cperson@example.com + >>> user_1.controls('cperson@example.com') + True + >>> address_3 = list(user_1.addresses)[0] + >>> user_manager.delete_address(address_3) + >>> dump_list(address.email for address in user_1.addresses) + *Empty* + >>> user_1.controls('cperson@example.com') + False + >>> dump_list(address.email for address in user_manager.addresses) + bperson@example.com + + +Registration and validation +=========================== + +Addresses have two dates, the date the address was registered on and the date +the address was validated on. The former is set when the address is created, +but the latter must be set explicitly. + + >>> address_4 = user_manager.create_address( + ... 'dperson@example.com', 'Dan Person') + >>> print address_4.registered_on + 2005-08-01 07:49:23 + >>> print address_4.verified_on + None + +The verification date records when the user has completed a mail-back +verification procedure. It takes a datetime object. + + >>> from mailman.utilities.datetime import now + >>> address_4.verified_on = now() + >>> print address_4.verified_on + 2005-08-01 07:49:23 + +The address shows the verified status in its repr. + + >>> address_4 + [verified] at ...> + + +Case-preserved addresses +======================== + +Technically speaking, email addresses are case sensitive in the local part. +Mailman preserves the case of addresses and uses the case preserved version +when sending the user a message, but it treats addresses that are different in +case equivalently in all other situations. + + >>> address_6 = user_manager.create_address( + ... 'FPERSON@example.com', 'Frank Person') + +The str() of such an address prints the RFC 2822 preferred originator format +with the original case-preserved address. The repr() contains all the gory +details. + + >>> print str(address_6) + Frank Person + >>> print repr(address_6) + [not verified] + key: fperson@example.com at 0x...> + +Both the case-insensitive version of the address and the original +case-preserved version are available on attributes of the `IAddress` object. + + >>> print address_6.email + fperson@example.com + >>> print address_6.original_email + FPERSON@example.com + +Because addresses are case-insensitive for all other purposes, you cannot +create an address that differs only in case. + + >>> user_manager.create_address('fperson@example.com') + Traceback (most recent call last): + ... + ExistingAddressError: FPERSON@example.com + >>> user_manager.create_address('fperson@EXAMPLE.COM') + Traceback (most recent call last): + ... + ExistingAddressError: FPERSON@example.com + >>> user_manager.create_address('FPERSON@example.com') + Traceback (most recent call last): + ... + ExistingAddressError: FPERSON@example.com + +You can get the address using either the lower cased version or case-preserved +version. In fact, searching for an address is case insensitive. + + >>> print user_manager.get_address('fperson@example.com').email + fperson@example.com + >>> print user_manager.get_address('FPERSON@example.com').email + fperson@example.com diff --git a/src/mailman/model/docs/addresses.txt b/src/mailman/model/docs/addresses.txt deleted file mode 100644 index 01e68c954..000000000 --- a/src/mailman/model/docs/addresses.txt +++ /dev/null @@ -1,204 +0,0 @@ -=============== -Email addresses -=============== - -Addresses represent a text email address, along with some meta data about -those addresses, such as their registration date, and whether and when they've -been validated. Addresses may be linked to the users that Mailman knows -about. Addresses are subscribed to mailing lists though members. - - >>> from mailman.interfaces.usermanager import IUserManager - >>> from zope.component import getUtility - >>> user_manager = getUtility(IUserManager) - - -Creating addresses -================== - -Addresses are created directly through the user manager, which starts out with -no addresses. - - >>> dump_list(address.email for address in user_manager.addresses) - *Empty* - -Creating an unlinked email address is straightforward. - - >>> address_1 = user_manager.create_address('aperson@example.com') - >>> dump_list(address.email for address in user_manager.addresses) - aperson@example.com - -However, such addresses have no real name. - - >>> print address_1.real_name - - -You can also create an email address object with a real name. - - >>> address_2 = user_manager.create_address( - ... 'bperson@example.com', 'Ben Person') - >>> dump_list(address.email for address in user_manager.addresses) - aperson@example.com - bperson@example.com - >>> dump_list(address.real_name for address in user_manager.addresses) - - Ben Person - -The ``str()`` of the address is the RFC 2822 preferred originator format, -while the ``repr()`` carries more information. - - >>> print str(address_2) - Ben Person - >>> print repr(address_2) - [not verified] at 0x...> - -You can assign real names to existing addresses. - - >>> address_1.real_name = 'Anne Person' - >>> dump_list(address.real_name for address in user_manager.addresses) - Anne Person - Ben Person - -These addresses are not linked to users, and can be seen by searching the user -manager for an associated user. - - >>> print user_manager.get_user('aperson@example.com') - None - >>> print user_manager.get_user('bperson@example.com') - None - -You can create email addresses that are linked to users by using a different -interface. - - >>> user_1 = user_manager.create_user( - ... 'cperson@example.com', u'Claire Person') - >>> dump_list(address.email for address in user_1.addresses) - cperson@example.com - >>> dump_list(address.email for address in user_manager.addresses) - aperson@example.com - bperson@example.com - cperson@example.com - >>> dump_list(address.real_name for address in user_manager.addresses) - Anne Person - Ben Person - Claire Person - -And now you can find the associated user. - - >>> print user_manager.get_user('aperson@example.com') - None - >>> print user_manager.get_user('bperson@example.com') - None - >>> user_manager.get_user('cperson@example.com') - - - -Deleting addresses -================== - -You can remove an unlinked address from the user manager. - - >>> user_manager.delete_address(address_1) - >>> dump_list(address.email for address in user_manager.addresses) - bperson@example.com - cperson@example.com - >>> dump_list(address.real_name for address in user_manager.addresses) - Ben Person - Claire Person - -Deleting a linked address does not delete the user, but it does unlink the -address from the user. - - >>> dump_list(address.email for address in user_1.addresses) - cperson@example.com - >>> user_1.controls('cperson@example.com') - True - >>> address_3 = list(user_1.addresses)[0] - >>> user_manager.delete_address(address_3) - >>> dump_list(address.email for address in user_1.addresses) - *Empty* - >>> user_1.controls('cperson@example.com') - False - >>> dump_list(address.email for address in user_manager.addresses) - bperson@example.com - - -Registration and validation -=========================== - -Addresses have two dates, the date the address was registered on and the date -the address was validated on. The former is set when the address is created, -but the latter must be set explicitly. - - >>> address_4 = user_manager.create_address( - ... 'dperson@example.com', 'Dan Person') - >>> print address_4.registered_on - 2005-08-01 07:49:23 - >>> print address_4.verified_on - None - -The verification date records when the user has completed a mail-back -verification procedure. It takes a datetime object. - - >>> from mailman.utilities.datetime import now - >>> address_4.verified_on = now() - >>> print address_4.verified_on - 2005-08-01 07:49:23 - -The address shows the verified status in its repr. - - >>> address_4 - [verified] at ...> - - -Case-preserved addresses -======================== - -Technically speaking, email addresses are case sensitive in the local part. -Mailman preserves the case of addresses and uses the case preserved version -when sending the user a message, but it treats addresses that are different in -case equivalently in all other situations. - - >>> address_6 = user_manager.create_address( - ... 'FPERSON@example.com', 'Frank Person') - -The str() of such an address prints the RFC 2822 preferred originator format -with the original case-preserved address. The repr() contains all the gory -details. - - >>> print str(address_6) - Frank Person - >>> print repr(address_6) - [not verified] - key: fperson@example.com at 0x...> - -Both the case-insensitive version of the address and the original -case-preserved version are available on attributes of the `IAddress` object. - - >>> print address_6.email - fperson@example.com - >>> print address_6.original_email - FPERSON@example.com - -Because addresses are case-insensitive for all other purposes, you cannot -create an address that differs only in case. - - >>> user_manager.create_address('fperson@example.com') - Traceback (most recent call last): - ... - ExistingAddressError: FPERSON@example.com - >>> user_manager.create_address('fperson@EXAMPLE.COM') - Traceback (most recent call last): - ... - ExistingAddressError: FPERSON@example.com - >>> user_manager.create_address('FPERSON@example.com') - Traceback (most recent call last): - ... - ExistingAddressError: FPERSON@example.com - -You can get the address using either the lower cased version or case-preserved -version. In fact, searching for an address is case insensitive. - - >>> print user_manager.get_address('fperson@example.com').email - fperson@example.com - >>> print user_manager.get_address('FPERSON@example.com').email - fperson@example.com diff --git a/src/mailman/model/docs/autorespond.rst b/src/mailman/model/docs/autorespond.rst new file mode 100644 index 000000000..3a9ad01b2 --- /dev/null +++ b/src/mailman/model/docs/autorespond.rst @@ -0,0 +1,116 @@ +=================== +Automatic responder +=================== + +In various situations, Mailman will send an automatic response to the author +of an email message. For example, if someone sends a command to the +``-request`` address, Mailman will send a response, but to cut down on third +party spam, the sender will only get a certain number of responses per day. + +First, given a mailing list you need to adapt it to an ``IAutoResponseSet``. +:: + + >>> mlist = create_list('test@example.com') + >>> from mailman.interfaces.autorespond import IAutoResponseSet + >>> response_set = IAutoResponseSet(mlist) + + >>> from zope.interface.verify import verifyObject + >>> verifyObject(IAutoResponseSet, response_set) + True + +You can't adapt other objects to an ``IAutoResponseSet``. + + >>> IAutoResponseSet(object()) + Traceback (most recent call last): + ... + TypeError: ('Could not adapt', ... + +There are various kinds of response types. For example, Mailman will send an +automatic response when messages are held for approval, or when it receives an +email command. You can find out how many responses for a particular address +have already been sent today. +:: + + >>> from mailman.interfaces.usermanager import IUserManager + >>> from zope.component import getUtility + >>> address = getUtility(IUserManager).create_address( + ... 'aperson@example.com') + + >>> from mailman.interfaces.autorespond import Response + >>> response_set.todays_count(address, Response.hold) + 0 + >>> response_set.todays_count(address, Response.command) + 0 + +Using the response set, we can record that a hold response is sent to the +address. + + >>> response_set.response_sent(address, Response.hold) + >>> response_set.todays_count(address, Response.hold) + 1 + >>> response_set.todays_count(address, Response.command) + 0 + +We can also record that a command response was sent. + + >>> response_set.response_sent(address, Response.command) + >>> response_set.todays_count(address, Response.hold) + 1 + >>> response_set.todays_count(address, Response.command) + 1 + +Let's send one more. + + >>> response_set.response_sent(address, Response.command) + >>> response_set.todays_count(address, Response.hold) + 1 + >>> response_set.todays_count(address, Response.command) + 2 + +Now the day flips over and all the counts reset. +:: + + >>> from mailman.utilities.datetime import factory + >>> factory.fast_forward() + + >>> response_set.todays_count(address, Response.hold) + 0 + >>> response_set.todays_count(address, Response.command) + 0 + + +Response dates +============== + +You can also use the response set to get the date of the last response sent. + + >>> response = response_set.last_response(address, Response.hold) + >>> response.mailing_list + + >>> response.address + + >>> response.response_type + + >>> response.date_sent + datetime.date(2005, 8, 1) + +When another response is sent today, that becomes the last one sent. +:: + + >>> response_set.response_sent(address, Response.command) + >>> response_set.last_response(address, Response.command).date_sent + datetime.date(2005, 8, 2) + + >>> factory.fast_forward(days=3) + >>> response_set.response_sent(address, Response.command) + >>> response_set.last_response(address, Response.command).date_sent + datetime.date(2005, 8, 5) + +If there's been no response sent to a particular address, None is returned. + + >>> address = getUtility(IUserManager).create_address( + ... 'bperson@example.com') + >>> response_set.todays_count(address, Response.command) + 0 + >>> print response_set.last_response(address, Response.command) + None diff --git a/src/mailman/model/docs/autorespond.txt b/src/mailman/model/docs/autorespond.txt deleted file mode 100644 index 3a9ad01b2..000000000 --- a/src/mailman/model/docs/autorespond.txt +++ /dev/null @@ -1,116 +0,0 @@ -=================== -Automatic responder -=================== - -In various situations, Mailman will send an automatic response to the author -of an email message. For example, if someone sends a command to the -``-request`` address, Mailman will send a response, but to cut down on third -party spam, the sender will only get a certain number of responses per day. - -First, given a mailing list you need to adapt it to an ``IAutoResponseSet``. -:: - - >>> mlist = create_list('test@example.com') - >>> from mailman.interfaces.autorespond import IAutoResponseSet - >>> response_set = IAutoResponseSet(mlist) - - >>> from zope.interface.verify import verifyObject - >>> verifyObject(IAutoResponseSet, response_set) - True - -You can't adapt other objects to an ``IAutoResponseSet``. - - >>> IAutoResponseSet(object()) - Traceback (most recent call last): - ... - TypeError: ('Could not adapt', ... - -There are various kinds of response types. For example, Mailman will send an -automatic response when messages are held for approval, or when it receives an -email command. You can find out how many responses for a particular address -have already been sent today. -:: - - >>> from mailman.interfaces.usermanager import IUserManager - >>> from zope.component import getUtility - >>> address = getUtility(IUserManager).create_address( - ... 'aperson@example.com') - - >>> from mailman.interfaces.autorespond import Response - >>> response_set.todays_count(address, Response.hold) - 0 - >>> response_set.todays_count(address, Response.command) - 0 - -Using the response set, we can record that a hold response is sent to the -address. - - >>> response_set.response_sent(address, Response.hold) - >>> response_set.todays_count(address, Response.hold) - 1 - >>> response_set.todays_count(address, Response.command) - 0 - -We can also record that a command response was sent. - - >>> response_set.response_sent(address, Response.command) - >>> response_set.todays_count(address, Response.hold) - 1 - >>> response_set.todays_count(address, Response.command) - 1 - -Let's send one more. - - >>> response_set.response_sent(address, Response.command) - >>> response_set.todays_count(address, Response.hold) - 1 - >>> response_set.todays_count(address, Response.command) - 2 - -Now the day flips over and all the counts reset. -:: - - >>> from mailman.utilities.datetime import factory - >>> factory.fast_forward() - - >>> response_set.todays_count(address, Response.hold) - 0 - >>> response_set.todays_count(address, Response.command) - 0 - - -Response dates -============== - -You can also use the response set to get the date of the last response sent. - - >>> response = response_set.last_response(address, Response.hold) - >>> response.mailing_list - - >>> response.address - - >>> response.response_type - - >>> response.date_sent - datetime.date(2005, 8, 1) - -When another response is sent today, that becomes the last one sent. -:: - - >>> response_set.response_sent(address, Response.command) - >>> response_set.last_response(address, Response.command).date_sent - datetime.date(2005, 8, 2) - - >>> factory.fast_forward(days=3) - >>> response_set.response_sent(address, Response.command) - >>> response_set.last_response(address, Response.command).date_sent - datetime.date(2005, 8, 5) - -If there's been no response sent to a particular address, None is returned. - - >>> address = getUtility(IUserManager).create_address( - ... 'bperson@example.com') - >>> response_set.todays_count(address, Response.command) - 0 - >>> print response_set.last_response(address, Response.command) - None diff --git a/src/mailman/model/docs/languages.rst b/src/mailman/model/docs/languages.rst new file mode 100644 index 000000000..21143f28b --- /dev/null +++ b/src/mailman/model/docs/languages.rst @@ -0,0 +1,112 @@ +========= +Languages +========= + +Mailman is multilingual. A language manager handles the known set of +languages at run time, as well as enabling those languages for use in a +running Mailman instance. +:: + + >>> from mailman.interfaces.languages import ILanguageManager + >>> from zope.component import getUtility + >>> from zope.interface.verify import verifyObject + + >>> mgr = getUtility(ILanguageManager) + >>> verifyObject(ILanguageManager, mgr) + True + + # The language manager component comes pre-populated; clear it out. + >>> mgr.clear() + +A language manager keeps track of the languages it knows about. + + >>> list(mgr.codes) + [] + >>> list(mgr.languages) + [] + + +Adding languages +================ + +Adding a new language requires three pieces of information, the 2-character +language code, the English description of the language, and the character set +used by the language. + + >>> mgr.add('en', 'us-ascii', 'English') + >>> mgr.add('it', 'iso-8859-1', 'Italian') + +And you can get information for all known languages. + + >>> print mgr['en'].description + English + >>> print mgr['en'].charset + us-ascii + >>> print mgr['it'].description + Italian + >>> print mgr['it'].charset + iso-8859-1 + + +Other iterations +================ + +You can iterate over all the known language codes. + + >>> mgr.add('pl', 'iso-8859-2', 'Polish') + >>> sorted(mgr.codes) + [u'en', u'it', u'pl'] + +You can iterate over all the known languages. + + >>> from operator import attrgetter + >>> languages = sorted((language for language in mgr.languages), + ... key=attrgetter('code')) + >>> for language in languages: + ... print language.code, language.charset, language.description + en us-ascii English + it iso-8859-1 Italian + pl iso-8859-2 Polish + +You can ask whether a particular language code is known. + + >>> 'it' in mgr + True + >>> 'xx' in mgr + False + +You can get a particular language by its code. + + >>> print mgr['it'].description + Italian + >>> print mgr['xx'].code + Traceback (most recent call last): + ... + KeyError: u'xx' + >>> print mgr.get('it').description + Italian + >>> print mgr.get('xx') + None + >>> print mgr.get('xx', 'missing') + missing + + +Clearing the known languages +============================ + +The language manager can forget about all the language codes it knows about. +:: + + >>> 'en' in mgr + True + + # Make a copy of the language manager's dictionary, so we can restore it + # after the test. Currently the test layer doesn't manage this. + >>> saved = mgr._languages.copy() + + >>> mgr.clear() + >>> 'en' in mgr + False + + # Restore the data. + >>> mgr._languages = saved diff --git a/src/mailman/model/docs/languages.txt b/src/mailman/model/docs/languages.txt deleted file mode 100644 index 21143f28b..000000000 --- a/src/mailman/model/docs/languages.txt +++ /dev/null @@ -1,112 +0,0 @@ -========= -Languages -========= - -Mailman is multilingual. A language manager handles the known set of -languages at run time, as well as enabling those languages for use in a -running Mailman instance. -:: - - >>> from mailman.interfaces.languages import ILanguageManager - >>> from zope.component import getUtility - >>> from zope.interface.verify import verifyObject - - >>> mgr = getUtility(ILanguageManager) - >>> verifyObject(ILanguageManager, mgr) - True - - # The language manager component comes pre-populated; clear it out. - >>> mgr.clear() - -A language manager keeps track of the languages it knows about. - - >>> list(mgr.codes) - [] - >>> list(mgr.languages) - [] - - -Adding languages -================ - -Adding a new language requires three pieces of information, the 2-character -language code, the English description of the language, and the character set -used by the language. - - >>> mgr.add('en', 'us-ascii', 'English') - >>> mgr.add('it', 'iso-8859-1', 'Italian') - -And you can get information for all known languages. - - >>> print mgr['en'].description - English - >>> print mgr['en'].charset - us-ascii - >>> print mgr['it'].description - Italian - >>> print mgr['it'].charset - iso-8859-1 - - -Other iterations -================ - -You can iterate over all the known language codes. - - >>> mgr.add('pl', 'iso-8859-2', 'Polish') - >>> sorted(mgr.codes) - [u'en', u'it', u'pl'] - -You can iterate over all the known languages. - - >>> from operator import attrgetter - >>> languages = sorted((language for language in mgr.languages), - ... key=attrgetter('code')) - >>> for language in languages: - ... print language.code, language.charset, language.description - en us-ascii English - it iso-8859-1 Italian - pl iso-8859-2 Polish - -You can ask whether a particular language code is known. - - >>> 'it' in mgr - True - >>> 'xx' in mgr - False - -You can get a particular language by its code. - - >>> print mgr['it'].description - Italian - >>> print mgr['xx'].code - Traceback (most recent call last): - ... - KeyError: u'xx' - >>> print mgr.get('it').description - Italian - >>> print mgr.get('xx') - None - >>> print mgr.get('xx', 'missing') - missing - - -Clearing the known languages -============================ - -The language manager can forget about all the language codes it knows about. -:: - - >>> 'en' in mgr - True - - # Make a copy of the language manager's dictionary, so we can restore it - # after the test. Currently the test layer doesn't manage this. - >>> saved = mgr._languages.copy() - - >>> mgr.clear() - >>> 'en' in mgr - False - - # Restore the data. - >>> mgr._languages = saved diff --git a/src/mailman/model/docs/listmanager.rst b/src/mailman/model/docs/listmanager.rst new file mode 100644 index 000000000..b571d9680 --- /dev/null +++ b/src/mailman/model/docs/listmanager.rst @@ -0,0 +1,99 @@ +======================== +The mailing list manager +======================== + +The ``IListManager`` is how you create, delete, and retrieve mailing list +objects. + + >>> from mailman.interfaces.listmanager import IListManager + >>> from zope.component import getUtility + >>> list_manager = getUtility(IListManager) + + +Creating a mailing list +======================= + +Creating the list returns the newly created IMailList object. + + >>> from mailman.interfaces.mailinglist import IMailingList + >>> mlist = list_manager.create('_xtest@example.com') + >>> IMailingList.providedBy(mlist) + True + +All lists with identities have a short name, a host name, and a fully +qualified listname. This latter is what uniquely distinguishes the mailing +list to the system. + + >>> print mlist.list_name + _xtest + >>> print mlist.mail_host + example.com + >>> print mlist.fqdn_listname + _xtest@example.com + +If you try to create a mailing list with the same name as an existing list, +you will get an exception. + + >>> list_manager.create('_xtest@example.com') + Traceback (most recent call last): + ... + ListAlreadyExistsError: _xtest@example.com + +It is an error to create a mailing list that isn't a fully qualified list name +(i.e. posting address). + + >>> list_manager.create('foo') + Traceback (most recent call last): + ... + InvalidEmailAddressError: foo + + +Deleting a mailing list +======================= + +Use the list manager to delete a mailing list. + + >>> list_manager.delete(mlist) + >>> sorted(list_manager.names) + [] + +After deleting the list, you can create it again. + + >>> mlist = list_manager.create('_xtest@example.com') + >>> print mlist.fqdn_listname + _xtest@example.com + + +Retrieving a mailing list +========================= + +When a mailing list exists, you can ask the list manager for it and you will +always get the same object back. + + >>> mlist_2 = list_manager.get('_xtest@example.com') + >>> mlist_2 is mlist + True + +If you try to get a list that doesn't existing yet, you get ``None``. + + >>> print list_manager.get('_xtest_2@example.com') + None + +You also get ``None`` if the list name is invalid. + + >>> print list_manager.get('foo') + None + + +Iterating over all mailing lists +================================ + +Once you've created a bunch of mailing lists, you can use the list manager to +iterate over either the list objects, or the list names. + + >>> mlist_3 = list_manager.create('_xtest_3@example.com') + >>> mlist_4 = list_manager.create('_xtest_4@example.com') + >>> sorted(list_manager.names) + [u'_xtest@example.com', u'_xtest_3@example.com', u'_xtest_4@example.com'] + >>> sorted(m.fqdn_listname for m in list_manager.mailing_lists) + [u'_xtest@example.com', u'_xtest_3@example.com', u'_xtest_4@example.com'] diff --git a/src/mailman/model/docs/listmanager.txt b/src/mailman/model/docs/listmanager.txt deleted file mode 100644 index b571d9680..000000000 --- a/src/mailman/model/docs/listmanager.txt +++ /dev/null @@ -1,99 +0,0 @@ -======================== -The mailing list manager -======================== - -The ``IListManager`` is how you create, delete, and retrieve mailing list -objects. - - >>> from mailman.interfaces.listmanager import IListManager - >>> from zope.component import getUtility - >>> list_manager = getUtility(IListManager) - - -Creating a mailing list -======================= - -Creating the list returns the newly created IMailList object. - - >>> from mailman.interfaces.mailinglist import IMailingList - >>> mlist = list_manager.create('_xtest@example.com') - >>> IMailingList.providedBy(mlist) - True - -All lists with identities have a short name, a host name, and a fully -qualified listname. This latter is what uniquely distinguishes the mailing -list to the system. - - >>> print mlist.list_name - _xtest - >>> print mlist.mail_host - example.com - >>> print mlist.fqdn_listname - _xtest@example.com - -If you try to create a mailing list with the same name as an existing list, -you will get an exception. - - >>> list_manager.create('_xtest@example.com') - Traceback (most recent call last): - ... - ListAlreadyExistsError: _xtest@example.com - -It is an error to create a mailing list that isn't a fully qualified list name -(i.e. posting address). - - >>> list_manager.create('foo') - Traceback (most recent call last): - ... - InvalidEmailAddressError: foo - - -Deleting a mailing list -======================= - -Use the list manager to delete a mailing list. - - >>> list_manager.delete(mlist) - >>> sorted(list_manager.names) - [] - -After deleting the list, you can create it again. - - >>> mlist = list_manager.create('_xtest@example.com') - >>> print mlist.fqdn_listname - _xtest@example.com - - -Retrieving a mailing list -========================= - -When a mailing list exists, you can ask the list manager for it and you will -always get the same object back. - - >>> mlist_2 = list_manager.get('_xtest@example.com') - >>> mlist_2 is mlist - True - -If you try to get a list that doesn't existing yet, you get ``None``. - - >>> print list_manager.get('_xtest_2@example.com') - None - -You also get ``None`` if the list name is invalid. - - >>> print list_manager.get('foo') - None - - -Iterating over all mailing lists -================================ - -Once you've created a bunch of mailing lists, you can use the list manager to -iterate over either the list objects, or the list names. - - >>> mlist_3 = list_manager.create('_xtest_3@example.com') - >>> mlist_4 = list_manager.create('_xtest_4@example.com') - >>> sorted(list_manager.names) - [u'_xtest@example.com', u'_xtest_3@example.com', u'_xtest_4@example.com'] - >>> sorted(m.fqdn_listname for m in list_manager.mailing_lists) - [u'_xtest@example.com', u'_xtest_3@example.com', u'_xtest_4@example.com'] diff --git a/src/mailman/model/docs/mailinglist.rst b/src/mailman/model/docs/mailinglist.rst new file mode 100644 index 000000000..895068e52 --- /dev/null +++ b/src/mailman/model/docs/mailinglist.rst @@ -0,0 +1,165 @@ +============= +Mailing lists +============= + +.. XXX 2010-06-18 BAW: This documentation needs a lot more detail. + +The mailing list is a core object in Mailman. It is uniquely identified in +the system by its posting address, i.e. the email address you would send a +message to in order to post a message to the mailing list. This must be fully +qualified. + + >>> mlist = create_list('aardvark@example.com') + >>> print mlist.fqdn_listname + aardvark@example.com + +The mailing list also has convenient attributes for accessing the list's short +name (i.e. local part) and host name. + + >>> print mlist.list_name + aardvark + >>> print mlist.mail_host + example.com + + +Rosters +======= + +Mailing list membership is represented by `rosters`. Each mailing list has +several rosters of members, representing the subscribers to the mailing list, +the owners, the moderators, and so on. The rosters are defined by a +membership role. + +Addresses can be explicitly subscribed to a mailing list. By default, a +subscription puts the address in the `member` role, meaning that address will +receive a copy of any message sent to the mailing list. +:: + + >>> from mailman.interfaces.usermanager import IUserManager + >>> from zope.component import getUtility + >>> user_manager = getUtility(IUserManager) + + >>> aperson = user_manager.create_address('aperson@example.com') + >>> bperson = user_manager.create_address('bperson@example.com') + >>> mlist.subscribe(aperson) + + >>> mlist.subscribe(bperson) + + +Both addresses appear on the roster of members. + + >>> for member in mlist.members.members: + ... print member + + + +By explicitly specifying the role of the subscription, an address can be added +to the owner and moderator rosters. + + >>> from mailman.interfaces.member import MemberRole + >>> mlist.subscribe(aperson, MemberRole.owner) + + >>> cperson = user_manager.create_address('cperson@example.com') + >>> mlist.subscribe(cperson, MemberRole.owner) + + >>> mlist.subscribe(cperson, MemberRole.moderator) + + +A Person is now both a member and an owner of the mailing list. C Person is +an owner and a moderator. +:: + + >>> for member in mlist.owners.members: + ... print member + + + + >>> for member in mlist.moderators.members: + ... print member + + + +All rosters can also be accessed indirectly. +:: + + >>> roster = mlist.get_roster(MemberRole.member) + >>> for member in roster.members: + ... print member + + + + >>> roster = mlist.get_roster(MemberRole.owner) + >>> for member in roster.members: + ... print member + + + + >>> roster = mlist.get_roster(MemberRole.moderator) + >>> for member in roster.members: + ... print member + + + +Subscribing users +================= + +An alternative way of subscribing to a mailing list is as a user with a +preferred address. This way the user can change their subscription address +just by changing their preferred address. +:: + + >>> from mailman.utilities.datetime import now + >>> user = user_manager.create_user('dperson@example.com', 'Dave Person') + >>> address = list(user.addresses)[0] + >>> address.verified_on = now() + >>> user.preferred_address = address + + >>> mlist.subscribe(user) + on aardvark@example.com + as MemberRole.member> + >>> for member in mlist.members.members: + ... print member + + + on aardvark@example.com + as MemberRole.member> + + >>> new_address = user.register('dave.person@example.com') + >>> new_address.verified_on = now() + >>> user.preferred_address = new_address + + >>> for member in mlist.members.members: + ... print member + + + + +A user is not allowed to subscribe more than once to the mailing list. + + >>> mlist.subscribe(user) + Traceback (most recent call last): + ... + AlreadySubscribedError: + is already a MemberRole.member of mailing list aardvark@example.com + +However, they are allowed to subscribe again with a specific address, even if +this address is their preferred address. + + >>> mlist.subscribe(user.preferred_address) + + +A user cannot subscribe to a mailing list without a preferred address. + + >>> user = user_manager.create_user('eperson@example.com', 'Elly Person') + >>> address = list(user.addresses)[0] + >>> address.verified_on = now() + >>> mlist.subscribe(user) + Traceback (most recent call last): + ... + MissingPreferredAddressError: User must have a preferred address: + diff --git a/src/mailman/model/docs/mailinglist.txt b/src/mailman/model/docs/mailinglist.txt deleted file mode 100644 index 895068e52..000000000 --- a/src/mailman/model/docs/mailinglist.txt +++ /dev/null @@ -1,165 +0,0 @@ -============= -Mailing lists -============= - -.. XXX 2010-06-18 BAW: This documentation needs a lot more detail. - -The mailing list is a core object in Mailman. It is uniquely identified in -the system by its posting address, i.e. the email address you would send a -message to in order to post a message to the mailing list. This must be fully -qualified. - - >>> mlist = create_list('aardvark@example.com') - >>> print mlist.fqdn_listname - aardvark@example.com - -The mailing list also has convenient attributes for accessing the list's short -name (i.e. local part) and host name. - - >>> print mlist.list_name - aardvark - >>> print mlist.mail_host - example.com - - -Rosters -======= - -Mailing list membership is represented by `rosters`. Each mailing list has -several rosters of members, representing the subscribers to the mailing list, -the owners, the moderators, and so on. The rosters are defined by a -membership role. - -Addresses can be explicitly subscribed to a mailing list. By default, a -subscription puts the address in the `member` role, meaning that address will -receive a copy of any message sent to the mailing list. -:: - - >>> from mailman.interfaces.usermanager import IUserManager - >>> from zope.component import getUtility - >>> user_manager = getUtility(IUserManager) - - >>> aperson = user_manager.create_address('aperson@example.com') - >>> bperson = user_manager.create_address('bperson@example.com') - >>> mlist.subscribe(aperson) - - >>> mlist.subscribe(bperson) - - -Both addresses appear on the roster of members. - - >>> for member in mlist.members.members: - ... print member - - - -By explicitly specifying the role of the subscription, an address can be added -to the owner and moderator rosters. - - >>> from mailman.interfaces.member import MemberRole - >>> mlist.subscribe(aperson, MemberRole.owner) - - >>> cperson = user_manager.create_address('cperson@example.com') - >>> mlist.subscribe(cperson, MemberRole.owner) - - >>> mlist.subscribe(cperson, MemberRole.moderator) - - -A Person is now both a member and an owner of the mailing list. C Person is -an owner and a moderator. -:: - - >>> for member in mlist.owners.members: - ... print member - - - - >>> for member in mlist.moderators.members: - ... print member - - - -All rosters can also be accessed indirectly. -:: - - >>> roster = mlist.get_roster(MemberRole.member) - >>> for member in roster.members: - ... print member - - - - >>> roster = mlist.get_roster(MemberRole.owner) - >>> for member in roster.members: - ... print member - - - - >>> roster = mlist.get_roster(MemberRole.moderator) - >>> for member in roster.members: - ... print member - - - -Subscribing users -================= - -An alternative way of subscribing to a mailing list is as a user with a -preferred address. This way the user can change their subscription address -just by changing their preferred address. -:: - - >>> from mailman.utilities.datetime import now - >>> user = user_manager.create_user('dperson@example.com', 'Dave Person') - >>> address = list(user.addresses)[0] - >>> address.verified_on = now() - >>> user.preferred_address = address - - >>> mlist.subscribe(user) - on aardvark@example.com - as MemberRole.member> - >>> for member in mlist.members.members: - ... print member - - - on aardvark@example.com - as MemberRole.member> - - >>> new_address = user.register('dave.person@example.com') - >>> new_address.verified_on = now() - >>> user.preferred_address = new_address - - >>> for member in mlist.members.members: - ... print member - - - - -A user is not allowed to subscribe more than once to the mailing list. - - >>> mlist.subscribe(user) - Traceback (most recent call last): - ... - AlreadySubscribedError: - is already a MemberRole.member of mailing list aardvark@example.com - -However, they are allowed to subscribe again with a specific address, even if -this address is their preferred address. - - >>> mlist.subscribe(user.preferred_address) - - -A user cannot subscribe to a mailing list without a preferred address. - - >>> user = user_manager.create_user('eperson@example.com', 'Elly Person') - >>> address = list(user.addresses)[0] - >>> address.verified_on = now() - >>> mlist.subscribe(user) - Traceback (most recent call last): - ... - MissingPreferredAddressError: User must have a preferred address: - diff --git a/src/mailman/model/docs/messagestore.rst b/src/mailman/model/docs/messagestore.rst new file mode 100644 index 000000000..3ee59129b --- /dev/null +++ b/src/mailman/model/docs/messagestore.rst @@ -0,0 +1,116 @@ +================= +The message store +================= + +The message store is a collection of messages keyed off of ``Message-ID`` and +``X-Message-ID-Hash`` headers. Either of these values can be combined with +the message's ``List-Archive`` header to create a globally unique URI to the +message object in the internet facing interface of the message store. The +``X-Message-ID-Hash`` is the Base32 SHA1 hash of the ``Message-ID``. + + >>> from mailman.interfaces.messages import IMessageStore + >>> from zope.component import getUtility + >>> message_store = getUtility(IMessageStore) + +If you try to add a message to the store which is missing the ``Message-ID`` +header, you will get an exception. + + >>> msg = message_from_string("""\ + ... Subject: An important message + ... + ... This message is very important. + ... """) + >>> message_store.add(msg) + Traceback (most recent call last): + ... + ValueError: Exactly one Message-ID header required + +However, if the message has a ``Message-ID`` header, it can be stored. + + >>> msg['Message-ID'] = '<87myycy5eh.fsf@uwakimon.sk.tsukuba.ac.jp>' + >>> message_store.add(msg) + 'AGDWSNXXKCWEILKKNYTBOHRDQGOX3Y35' + >>> print msg.as_string() + Subject: An important message + Message-ID: <87myycy5eh.fsf@uwakimon.sk.tsukuba.ac.jp> + X-Message-ID-Hash: AGDWSNXXKCWEILKKNYTBOHRDQGOX3Y35 + + This message is very important. + + + +Finding messages +================ + +There are several ways to find a message given either the ``Message-ID`` or +``X-Message-ID-Hash`` headers. In either case, if no matching message is +found, ``None`` is returned. + + >>> print message_store.get_message_by_id('nothing') + None + >>> print message_store.get_message_by_hash('nothing') + None + +Given an existing ``Message-ID``, the message can be found. + + >>> message = message_store.get_message_by_id(msg['message-id']) + >>> print message.as_string() + Subject: An important message + Message-ID: <87myycy5eh.fsf@uwakimon.sk.tsukuba.ac.jp> + X-Message-ID-Hash: AGDWSNXXKCWEILKKNYTBOHRDQGOX3Y35 + + This message is very important. + + +Similarly, we can find messages by the ``X-Message-ID-Hash``: + + >>> message = message_store.get_message_by_hash(msg['x-message-id-hash']) + >>> print message.as_string() + Subject: An important message + Message-ID: <87myycy5eh.fsf@uwakimon.sk.tsukuba.ac.jp> + X-Message-ID-Hash: AGDWSNXXKCWEILKKNYTBOHRDQGOX3Y35 + + This message is very important. + + + +Iterating over all messages +=========================== + +The message store provides a means to iterate over all the messages it +contains. + + >>> messages = list(message_store.messages) + >>> len(messages) + 1 + >>> print messages[0].as_string() + Subject: An important message + Message-ID: <87myycy5eh.fsf@uwakimon.sk.tsukuba.ac.jp> + X-Message-ID-Hash: AGDWSNXXKCWEILKKNYTBOHRDQGOX3Y35 + + This message is very important. + + + +Deleting messages from the store +================================ + +You delete a message from the storage service by providing the ``Message-ID`` +for the message you want to delete. If you try to delete a ``Message-ID`` +that isn't in the store, you get an exception. + + >>> message_store.delete_message('nothing') + Traceback (most recent call last): + ... + LookupError: nothing + +But if you delete an existing message, it really gets deleted. + + >>> message_id = message['message-id'] + >>> message_store.delete_message(message_id) + >>> list(message_store.messages) + [] + >>> print message_store.get_message_by_id(message_id) + None + >>> print message_store.get_message_by_hash(message['x-message-id-hash']) + None diff --git a/src/mailman/model/docs/messagestore.txt b/src/mailman/model/docs/messagestore.txt deleted file mode 100644 index 3ee59129b..000000000 --- a/src/mailman/model/docs/messagestore.txt +++ /dev/null @@ -1,116 +0,0 @@ -================= -The message store -================= - -The message store is a collection of messages keyed off of ``Message-ID`` and -``X-Message-ID-Hash`` headers. Either of these values can be combined with -the message's ``List-Archive`` header to create a globally unique URI to the -message object in the internet facing interface of the message store. The -``X-Message-ID-Hash`` is the Base32 SHA1 hash of the ``Message-ID``. - - >>> from mailman.interfaces.messages import IMessageStore - >>> from zope.component import getUtility - >>> message_store = getUtility(IMessageStore) - -If you try to add a message to the store which is missing the ``Message-ID`` -header, you will get an exception. - - >>> msg = message_from_string("""\ - ... Subject: An important message - ... - ... This message is very important. - ... """) - >>> message_store.add(msg) - Traceback (most recent call last): - ... - ValueError: Exactly one Message-ID header required - -However, if the message has a ``Message-ID`` header, it can be stored. - - >>> msg['Message-ID'] = '<87myycy5eh.fsf@uwakimon.sk.tsukuba.ac.jp>' - >>> message_store.add(msg) - 'AGDWSNXXKCWEILKKNYTBOHRDQGOX3Y35' - >>> print msg.as_string() - Subject: An important message - Message-ID: <87myycy5eh.fsf@uwakimon.sk.tsukuba.ac.jp> - X-Message-ID-Hash: AGDWSNXXKCWEILKKNYTBOHRDQGOX3Y35 - - This message is very important. - - - -Finding messages -================ - -There are several ways to find a message given either the ``Message-ID`` or -``X-Message-ID-Hash`` headers. In either case, if no matching message is -found, ``None`` is returned. - - >>> print message_store.get_message_by_id('nothing') - None - >>> print message_store.get_message_by_hash('nothing') - None - -Given an existing ``Message-ID``, the message can be found. - - >>> message = message_store.get_message_by_id(msg['message-id']) - >>> print message.as_string() - Subject: An important message - Message-ID: <87myycy5eh.fsf@uwakimon.sk.tsukuba.ac.jp> - X-Message-ID-Hash: AGDWSNXXKCWEILKKNYTBOHRDQGOX3Y35 - - This message is very important. - - -Similarly, we can find messages by the ``X-Message-ID-Hash``: - - >>> message = message_store.get_message_by_hash(msg['x-message-id-hash']) - >>> print message.as_string() - Subject: An important message - Message-ID: <87myycy5eh.fsf@uwakimon.sk.tsukuba.ac.jp> - X-Message-ID-Hash: AGDWSNXXKCWEILKKNYTBOHRDQGOX3Y35 - - This message is very important. - - - -Iterating over all messages -=========================== - -The message store provides a means to iterate over all the messages it -contains. - - >>> messages = list(message_store.messages) - >>> len(messages) - 1 - >>> print messages[0].as_string() - Subject: An important message - Message-ID: <87myycy5eh.fsf@uwakimon.sk.tsukuba.ac.jp> - X-Message-ID-Hash: AGDWSNXXKCWEILKKNYTBOHRDQGOX3Y35 - - This message is very important. - - - -Deleting messages from the store -================================ - -You delete a message from the storage service by providing the ``Message-ID`` -for the message you want to delete. If you try to delete a ``Message-ID`` -that isn't in the store, you get an exception. - - >>> message_store.delete_message('nothing') - Traceback (most recent call last): - ... - LookupError: nothing - -But if you delete an existing message, it really gets deleted. - - >>> message_id = message['message-id'] - >>> message_store.delete_message(message_id) - >>> list(message_store.messages) - [] - >>> print message_store.get_message_by_id(message_id) - None - >>> print message_store.get_message_by_hash(message['x-message-id-hash']) - None diff --git a/src/mailman/model/docs/mlist-addresses.rst b/src/mailman/model/docs/mlist-addresses.rst new file mode 100644 index 000000000..2a021f67f --- /dev/null +++ b/src/mailman/model/docs/mlist-addresses.rst @@ -0,0 +1,78 @@ +====================== +Mailing list addresses +====================== + +Every mailing list has a number of addresses which are publicly available. +These are defined in the ``IMailingListAddresses`` interface. + + >>> mlist = create_list('_xtest@example.com') + +The posting address is where people send messages to be posted to the mailing +list. This is exactly the same as the fully qualified list name. + + >>> print mlist.fqdn_listname + _xtest@example.com + >>> print mlist.posting_address + _xtest@example.com + +Messages to the mailing list's `no reply` address always get discarded without +prejudice. + + >>> print mlist.no_reply_address + noreply@example.com + +The mailing list's owner address reaches the human moderators. + + >>> print mlist.owner_address + _xtest-owner@example.com + +The request address goes to the list's email command robot. + + >>> print mlist.request_address + _xtest-request@example.com + +The bounces address accepts and processes all potential bounces. + + >>> print mlist.bounces_address + _xtest-bounces@example.com + +The join (a.k.a. subscribe) address is where someone can email to get added to +the mailing list. The subscribe alias is a synonym for join, but it's +deprecated. + + >>> print mlist.join_address + _xtest-join@example.com + >>> print mlist.subscribe_address + _xtest-subscribe@example.com + +The leave (a.k.a. unsubscribe) address is where someone can email to get added +to the mailing list. The unsubscribe alias is a synonym for leave, but it's +deprecated. + + >>> print mlist.leave_address + _xtest-leave@example.com + >>> print mlist.unsubscribe_address + _xtest-unsubscribe@example.com + + +Email confirmations +=================== + +Email confirmation messages are sent when actions such as subscriptions need +to be confirmed. It requires that a cookie be provided, which will be +included in the local part of the email address. The exact format of this is +dependent on the ``verp_confirm_format`` configuration variable. +:: + + >>> print mlist.confirm_address('cookie') + _xtest-confirm+cookie@example.com + >>> print mlist.confirm_address('wookie') + _xtest-confirm+wookie@example.com + + >>> config.push('test config', """ + ... [mta] + ... verp_confirm_format: $address---$cookie + ... """) + >>> print mlist.confirm_address('cookie') + _xtest-confirm---cookie@example.com + >>> config.pop('test config') diff --git a/src/mailman/model/docs/mlist-addresses.txt b/src/mailman/model/docs/mlist-addresses.txt deleted file mode 100644 index 2a021f67f..000000000 --- a/src/mailman/model/docs/mlist-addresses.txt +++ /dev/null @@ -1,78 +0,0 @@ -====================== -Mailing list addresses -====================== - -Every mailing list has a number of addresses which are publicly available. -These are defined in the ``IMailingListAddresses`` interface. - - >>> mlist = create_list('_xtest@example.com') - -The posting address is where people send messages to be posted to the mailing -list. This is exactly the same as the fully qualified list name. - - >>> print mlist.fqdn_listname - _xtest@example.com - >>> print mlist.posting_address - _xtest@example.com - -Messages to the mailing list's `no reply` address always get discarded without -prejudice. - - >>> print mlist.no_reply_address - noreply@example.com - -The mailing list's owner address reaches the human moderators. - - >>> print mlist.owner_address - _xtest-owner@example.com - -The request address goes to the list's email command robot. - - >>> print mlist.request_address - _xtest-request@example.com - -The bounces address accepts and processes all potential bounces. - - >>> print mlist.bounces_address - _xtest-bounces@example.com - -The join (a.k.a. subscribe) address is where someone can email to get added to -the mailing list. The subscribe alias is a synonym for join, but it's -deprecated. - - >>> print mlist.join_address - _xtest-join@example.com - >>> print mlist.subscribe_address - _xtest-subscribe@example.com - -The leave (a.k.a. unsubscribe) address is where someone can email to get added -to the mailing list. The unsubscribe alias is a synonym for leave, but it's -deprecated. - - >>> print mlist.leave_address - _xtest-leave@example.com - >>> print mlist.unsubscribe_address - _xtest-unsubscribe@example.com - - -Email confirmations -=================== - -Email confirmation messages are sent when actions such as subscriptions need -to be confirmed. It requires that a cookie be provided, which will be -included in the local part of the email address. The exact format of this is -dependent on the ``verp_confirm_format`` configuration variable. -:: - - >>> print mlist.confirm_address('cookie') - _xtest-confirm+cookie@example.com - >>> print mlist.confirm_address('wookie') - _xtest-confirm+wookie@example.com - - >>> config.push('test config', """ - ... [mta] - ... verp_confirm_format: $address---$cookie - ... """) - >>> print mlist.confirm_address('cookie') - _xtest-confirm---cookie@example.com - >>> config.pop('test config') diff --git a/src/mailman/model/docs/registration.rst b/src/mailman/model/docs/registration.rst new file mode 100644 index 000000000..9605fcbea --- /dev/null +++ b/src/mailman/model/docs/registration.rst @@ -0,0 +1,352 @@ +==================== +Address registration +==================== + +Before users can join a mailing list, they must first register with Mailman. +The only thing they must supply is an email address, although there is +additional information they may supply. All registered email addresses must +be verified before Mailman will send them any list traffic. + +The ``IUserManager`` manages users, but it does so at a fairly low level. +Specifically, it does not handle verifications, email address syntax validity +checks, etc. The ``IRegistrar`` is the interface to the object handling all +this stuff. + + >>> from mailman.interfaces.registrar import IRegistrar + >>> from zope.component import getUtility + >>> registrar = getUtility(IRegistrar) + +Here is a helper function to check the token strings. + + >>> def check_token(token): + ... assert isinstance(token, basestring), 'Not a string' + ... assert len(token) == 40, 'Unexpected length: %d' % len(token) + ... assert token.isalnum(), 'Not alphanumeric' + ... print 'ok' + +Here is a helper function to extract tokens from confirmation messages. + + >>> import re + >>> cre = re.compile('http://lists.example.com/confirm/(.*)') + >>> def extract_token(msg): + ... mo = cre.search(msg.get_payload()) + ... return mo.group(1) + + +Invalid email addresses +======================= + +Addresses are registered within the context of a mailing list, mostly so that +confirmation emails can come from some place. You also need the email +address of the user who is registering. + + >>> mlist = create_list('alpha@example.com') + +Some amount of sanity checks are performed on the email address, although +honestly, not as much as probably should be done. Still, some patently bad +addresses are rejected outright. + + >>> registrar.register(mlist, '') + Traceback (most recent call last): + ... + InvalidEmailAddressError + >>> registrar.register(mlist, 'some name@example.com') + Traceback (most recent call last): + ... + InvalidEmailAddressError: some name@example.com + >>> registrar.register(mlist, '