From eefd06f1b88b8ecbb23a9013cd223b72ca85c20d Mon Sep 17 00:00:00 2001 From: Barry Warsaw Date: Sun, 25 Jan 2009 13:01:41 -0500 Subject: Push the source directory into a 'src' subdirectory so that zc.buildout works correctly regardless of how it's used. --- .bzrignore | 1 + mailman/Archiver/Archiver.py | 230 - mailman/Archiver/HyperArch.py | 1237 -- mailman/Archiver/HyperDatabase.py | 339 - mailman/Archiver/__init__.py | 18 - mailman/Archiver/pipermail.py | 874 - mailman/Bouncers/BouncerAPI.py | 64 - mailman/Bouncers/Caiwireless.py | 45 - mailman/Bouncers/Compuserve.py | 46 - mailman/Bouncers/DSN.py | 99 - mailman/Bouncers/Exchange.py | 48 - mailman/Bouncers/Exim.py | 31 - mailman/Bouncers/GroupWise.py | 71 - mailman/Bouncers/LLNL.py | 32 - mailman/Bouncers/Microsoft.py | 53 - mailman/Bouncers/Netscape.py | 89 - mailman/Bouncers/Postfix.py | 86 - mailman/Bouncers/Qmail.py | 72 - mailman/Bouncers/SMTP32.py | 60 - mailman/Bouncers/SimpleMatch.py | 204 - mailman/Bouncers/SimpleWarning.py | 62 - mailman/Bouncers/Sina.py | 48 - mailman/Bouncers/Yahoo.py | 54 - mailman/Bouncers/Yale.py | 80 - mailman/Bouncers/__init__.py | 0 mailman/Mailbox.py | 106 - mailman/Message.py | 297 - mailman/Utils.py | 702 - mailman/__init__.py | 0 mailman/app/__init__.py | 0 mailman/app/bounces.py | 63 - mailman/app/commands.py | 44 - mailman/app/lifecycle.py | 114 - mailman/app/membership.py | 137 - mailman/app/moderator.py | 351 - mailman/app/notifications.py | 136 - mailman/app/registrar.py | 163 - mailman/app/replybot.py | 125 - mailman/archiving/__init__.py | 0 mailman/archiving/mailarchive.py | 87 - mailman/archiving/mhonarc.py | 97 - mailman/archiving/pipermail.py | 121 - mailman/archiving/prototype.py | 77 - mailman/attic/Bouncer.py | 250 - mailman/attic/Defaults.py | 1324 -- mailman/attic/Deliverer.py | 174 - mailman/attic/Digester.py | 57 - mailman/attic/MailList.py | 731 - mailman/attic/SecurityManager.py | 306 - mailman/attic/bin/clone_member | 219 - mailman/attic/bin/discard | 120 - mailman/attic/bin/fix_url.py | 93 - mailman/attic/bin/list_admins | 101 - mailman/attic/bin/msgfmt.py | 203 - mailman/attic/bin/po2templ.py | 90 - mailman/attic/bin/pygettext.py | 545 - mailman/attic/bin/remove_members | 186 - mailman/attic/bin/reset_pw.py | 83 - mailman/attic/bin/sync_members | 286 - mailman/attic/bin/templ2pot.py | 120 - mailman/attic/bin/transcheck | 412 - mailman/bin/__init__.py | 61 - mailman/bin/add_members.py | 186 - mailman/bin/arch.py | 151 - mailman/bin/bumpdigests.py | 74 - mailman/bin/check_perms.py | 408 - mailman/bin/checkdbs.py | 199 - mailman/bin/cleanarch.py | 133 - mailman/bin/config_list.py | 332 - mailman/bin/create_list.py | 129 - mailman/bin/disabled.py | 201 - mailman/bin/docs/master.txt | 49 - mailman/bin/dumpdb.py | 88 - mailman/bin/export.py | 310 - mailman/bin/find_member.py | 135 - mailman/bin/gate_news.py | 243 - mailman/bin/genaliases.py | 64 - mailman/bin/import.py | 315 - mailman/bin/inject.py | 89 - mailman/bin/list_lists.py | 104 - mailman/bin/list_members.py | 201 - mailman/bin/list_owners.py | 88 - mailman/bin/mailmanctl.py | 232 - mailman/bin/master.py | 452 - mailman/bin/mmsitepass.py | 113 - mailman/bin/nightly_gzip.py | 117 - mailman/bin/qrunner.py | 269 - mailman/bin/remove_list.py | 83 - mailman/bin/senddigests.py | 83 - mailman/bin/set_members.py | 189 - mailman/bin/show_config.py | 97 - mailman/bin/show_qfiles.py | 91 - mailman/bin/unshunt.py | 51 - mailman/bin/update.py | 660 - mailman/bin/version.py | 46 - mailman/bin/withlist.py | 220 - mailman/chains/__init__.py | 0 mailman/chains/accept.py | 58 - mailman/chains/base.py | 122 - mailman/chains/builtin.py | 86 - mailman/chains/discard.py | 47 - mailman/chains/headers.py | 156 - mailman/chains/hold.py | 178 - mailman/chains/reject.py | 59 - mailman/commands/__init__.py | 22 - mailman/commands/cmd_confirm.py | 98 - mailman/commands/cmd_help.py | 93 - mailman/commands/cmd_info.py | 50 - mailman/commands/cmd_leave.py | 21 - mailman/commands/cmd_lists.py | 65 - mailman/commands/cmd_password.py | 123 - mailman/commands/cmd_remove.py | 21 - mailman/commands/cmd_set.py | 360 - mailman/commands/cmd_unsubscribe.py | 88 - mailman/commands/cmd_who.py | 152 - mailman/commands/docs/echo.txt | 30 - mailman/commands/docs/end.txt | 37 - mailman/commands/docs/join.txt | 170 - mailman/commands/echo.py | 48 - mailman/commands/end.py | 51 - mailman/commands/join.py | 126 - mailman/config/__init__.py | 30 - mailman/config/config.py | 206 - mailman/config/mailman.cfg | 69 - mailman/config/schema.cfg | 589 - mailman/constants.py | 44 - mailman/core/__init__.py | 0 mailman/core/chains.py | 118 - mailman/core/errors.py | 172 - mailman/core/initialize.py | 125 - mailman/core/logging.py | 163 - mailman/core/pipelines.py | 125 - mailman/core/plugins.py | 74 - mailman/core/rules.py | 46 - mailman/database/__init__.py | 153 - mailman/database/address.py | 96 - mailman/database/language.py | 40 - mailman/database/listmanager.py | 82 - mailman/database/mailinglist.py | 272 - mailman/database/mailman.sql | 208 - mailman/database/member.py | 105 - mailman/database/message.py | 53 - mailman/database/messagestore.py | 137 - mailman/database/model.py | 56 - mailman/database/pending.py | 177 - mailman/database/preferences.py | 50 - mailman/database/requests.py | 138 - mailman/database/roster.py | 270 - mailman/database/transaction.py | 53 - mailman/database/types.py | 64 - mailman/database/user.py | 94 - mailman/database/usermanager.py | 103 - mailman/database/version.py | 40 - mailman/docs/__init__.py | 0 mailman/docs/addresses.txt | 231 - mailman/docs/archivers.txt | 184 - mailman/docs/bounces.txt | 107 - mailman/docs/chains.txt | 345 - mailman/docs/domains.txt | 46 - mailman/docs/languages.txt | 104 - mailman/docs/lifecycle.txt | 136 - mailman/docs/listmanager.txt | 88 - mailman/docs/membership.txt | 230 - mailman/docs/message.txt | 48 - mailman/docs/messagestore.txt | 113 - mailman/docs/mlist-addresses.txt | 76 - mailman/docs/pending.txt | 94 - mailman/docs/pipelines.txt | 186 - mailman/docs/registration.txt | 362 - mailman/docs/requests.txt | 883 - mailman/docs/styles.txt | 156 - mailman/docs/usermanager.txt | 124 - mailman/docs/users.txt | 195 - mailman/domain.py | 70 - mailman/extras/__init__.py | 0 mailman/extras/mailman.cfg.in | 46 - mailman/i18n.py | 198 - mailman/inject.py | 86 - mailman/interact.py | 75 - mailman/interfaces/__init__.py | 44 - mailman/interfaces/address.py | 101 - mailman/interfaces/archiver.py | 78 - mailman/interfaces/chain.py | 110 - mailman/interfaces/command.py | 68 - mailman/interfaces/database.py | 97 - mailman/interfaces/domain.py | 85 - mailman/interfaces/errors.py | 35 - mailman/interfaces/handler.py | 45 - mailman/interfaces/languages.py | 89 - mailman/interfaces/listmanager.py | 84 - mailman/interfaces/mailinglist.py | 273 - mailman/interfaces/member.py | 187 - mailman/interfaces/messages.py | 111 - mailman/interfaces/mlistrequest.py | 37 - mailman/interfaces/mta.py | 42 - mailman/interfaces/pending.py | 99 - mailman/interfaces/permissions.py | 36 - mailman/interfaces/pipeline.py | 40 - mailman/interfaces/preferences.py | 77 - mailman/interfaces/registrar.py | 83 - mailman/interfaces/requests.py | 115 - mailman/interfaces/roster.py | 61 - mailman/interfaces/rules.py | 53 - mailman/interfaces/runner.py | 119 - mailman/interfaces/styles.py | 120 - mailman/interfaces/switchboard.py | 90 - mailman/interfaces/user.py | 84 - mailman/interfaces/usermanager.py | 96 - mailman/languages.py | 67 - mailman/messages/__init__.py | 0 mailman/messages/ar/LC_MESSAGES/mailman.po | 15702 ----------------- mailman/messages/ca/LC_MESSAGES/mailman.po | 15421 ----------------- mailman/messages/cs/LC_MESSAGES/mailman.po | 13393 --------------- mailman/messages/da/LC_MESSAGES/mailman.po | 15972 ----------------- mailman/messages/de/LC_MESSAGES/mailman.po | 15318 ----------------- mailman/messages/de/README.de | 21 - mailman/messages/docstring.files | 59 - mailman/messages/es/LC_MESSAGES/mailman.po | 16349 ------------------ mailman/messages/es/README.es | 82 - mailman/messages/et/LC_MESSAGES/mailman.po | 14338 ---------------- mailman/messages/eu/LC_MESSAGES/mailman.po | 14145 --------------- mailman/messages/eu/README.eu | 103 - mailman/messages/fi/LC_MESSAGES/mailman.po | 13862 --------------- mailman/messages/fi/README.fi | 13 - mailman/messages/fr/LC_MESSAGES/mailman.po | 15420 ----------------- mailman/messages/fr/README.fr | 7 - mailman/messages/hr/LC_MESSAGES/mailman.po | 13737 --------------- mailman/messages/hu/FAQ.hu | 464 - mailman/messages/hu/INSTALL.hu | 640 - mailman/messages/hu/LC_MESSAGES/mailman.po | 14882 ---------------- mailman/messages/hu/README.BSD.hu | 28 - mailman/messages/hu/README.CONTRIB.hu | 17 - mailman/messages/hu/README.EXIM.hu | 359 - mailman/messages/hu/README.LINUX.hu | 59 - mailman/messages/hu/README.MACOSX.hu | 31 - mailman/messages/hu/README.NETSCAPE.hu | 57 - mailman/messages/hu/README.POSTFIX.hu | 239 - mailman/messages/hu/README.QMAIL.hu | 186 - mailman/messages/hu/README.SENDMAIL.hu | 86 - mailman/messages/hu/README.USERAGENT.hu | 49 - mailman/messages/hu/README.hu | 271 - mailman/messages/hu/UPGRADING.hu | 391 - mailman/messages/ia/LC_MESSAGES/mailman.po | 14426 ---------------- mailman/messages/it/LC_MESSAGES/mailman.po | 15623 ----------------- mailman/messages/it/README.it | 32 - mailman/messages/ja/INSTALL | 615 - mailman/messages/ja/LC_MESSAGES/mailman.po | 14328 ---------------- mailman/messages/ja/README | 214 - mailman/messages/ja/README.ja | 109 - mailman/messages/ja/UPGRADING | 215 - mailman/messages/ja/doc/Defaults.py.in | 1442 -- mailman/messages/ja/doc/mailman-install.tex | 1933 --- mailman/messages/ja/doc/mailman-member.tex | 1787 -- mailman/messages/ko/LC_MESSAGES/mailman.po | 12970 -------------- mailman/messages/ko/README.ko | 26 - mailman/messages/lt/LC_MESSAGES/mailman.po | 12116 ------------- mailman/messages/mailman.pot | 10031 ----------- mailman/messages/marked.files | 130 - mailman/messages/nl/LC_MESSAGES/mailman.po | 13629 --------------- mailman/messages/no/LC_MESSAGES/mailman.po | 15503 ----------------- mailman/messages/pl/LC_MESSAGES/mailman.po | 13120 -------------- mailman/messages/pl/README.pl | 28 - mailman/messages/pt/LC_MESSAGES/mailman.po | 14924 ---------------- mailman/messages/pt_BR/LC_MESSAGES/mailman.po | 15033 ---------------- mailman/messages/ro/LC_MESSAGES/mailman.po | 14633 ---------------- mailman/messages/ru/LC_MESSAGES/mailman.po | 14862 ---------------- mailman/messages/ru/README.ru | 17 - mailman/messages/sl/LC_MESSAGES/mailman.po | 17650 ------------------- mailman/messages/sr/LC_MESSAGES/mailman.po | 11873 ------------- mailman/messages/sr/readme.sr | 6 - mailman/messages/sv/LC_MESSAGES/mailman.po | 18097 -------------------- mailman/messages/sv/README.sv | 30 - mailman/messages/tr/LC_MESSAGES/mailman.po | 13641 --------------- mailman/messages/uk/LC_MESSAGES/mailman.po | 14897 ---------------- mailman/messages/vi/LC_MESSAGES/mailman.po | 14584 ---------------- mailman/messages/zh_CN/LC_MESSAGES/mailman.po | 14142 --------------- mailman/messages/zh_TW/LC_MESSAGES/mailman.po | 12964 -------------- mailman/mta/__init__.py | 0 mailman/mta/null.py | 51 - mailman/mta/postfix.py | 123 - mailman/mta/smtp_direct.py | 417 - mailman/options.py | 143 - mailman/passwords.py | 253 - mailman/pipeline/__init__.py | 54 - mailman/pipeline/acknowledge.py | 80 - mailman/pipeline/after_delivery.py | 48 - mailman/pipeline/avoid_duplicates.py | 116 - mailman/pipeline/calculate_recipients.py | 148 - mailman/pipeline/cleanse.py | 75 - mailman/pipeline/cleanse_dkim.py | 58 - mailman/pipeline/cook_headers.py | 357 - mailman/pipeline/decorate.py | 231 - mailman/pipeline/docs/ack-headers.txt | 40 - mailman/pipeline/docs/acknowledge.txt | 159 - mailman/pipeline/docs/after-delivery.txt | 27 - mailman/pipeline/docs/archives.txt | 133 - mailman/pipeline/docs/avoid-duplicates.txt | 168 - mailman/pipeline/docs/calc-recips.txt | 100 - mailman/pipeline/docs/cleanse.txt | 94 - mailman/pipeline/docs/cook-headers.txt | 326 - mailman/pipeline/docs/decorate.txt | 317 - mailman/pipeline/docs/digests.txt | 535 - mailman/pipeline/docs/file-recips.txt | 96 - mailman/pipeline/docs/filtering.txt | 340 - mailman/pipeline/docs/nntp.txt | 65 - mailman/pipeline/docs/reply-to.txt | 127 - mailman/pipeline/docs/replybot.txt | 213 - mailman/pipeline/docs/scrubber.txt | 225 - mailman/pipeline/docs/subject-munging.txt | 244 - mailman/pipeline/docs/tagger.txt | 235 - mailman/pipeline/docs/to-outgoing.txt | 173 - mailman/pipeline/file_recipients.py | 65 - mailman/pipeline/mime_delete.py | 285 - mailman/pipeline/moderate.py | 175 - mailman/pipeline/owner_recipients.py | 34 - mailman/pipeline/replybot.py | 134 - mailman/pipeline/scrubber.py | 509 - mailman/pipeline/tagger.py | 187 - mailman/pipeline/to_archive.py | 55 - mailman/pipeline/to_digest.py | 440 - mailman/pipeline/to_outgoing.py | 78 - mailman/pipeline/to_usenet.py | 69 - mailman/queue/__init__.py | 466 - mailman/queue/archive.py | 91 - mailman/queue/bounce.py | 316 - mailman/queue/command.py | 214 - mailman/queue/docs/OVERVIEW.txt | 78 - mailman/queue/docs/archiver.txt | 34 - mailman/queue/docs/command.txt | 170 - mailman/queue/docs/incoming.txt | 200 - mailman/queue/docs/lmtp.txt | 103 - mailman/queue/docs/news.txt | 157 - mailman/queue/docs/outgoing.txt | 75 - mailman/queue/docs/runner.txt | 72 - mailman/queue/docs/switchboard.txt | 182 - mailman/queue/http.py | 73 - mailman/queue/incoming.py | 43 - mailman/queue/lmtp.py | 218 - mailman/queue/maildir.py | 190 - mailman/queue/news.py | 166 - mailman/queue/outgoing.py | 130 - mailman/queue/pipeline.py | 35 - mailman/queue/retry.py | 37 - mailman/queue/virgin.py | 39 - mailman/rules/__init__.py | 54 - mailman/rules/administrivia.py | 102 - mailman/rules/any.py | 45 - mailman/rules/approved.py | 121 - mailman/rules/docs/administrivia.txt | 99 - mailman/rules/docs/approve.txt | 472 - mailman/rules/docs/emergency.txt | 72 - mailman/rules/docs/header-matching.txt | 144 - mailman/rules/docs/implicit-dest.txt | 75 - mailman/rules/docs/loop.txt | 48 - mailman/rules/docs/max-size.txt | 39 - mailman/rules/docs/moderation.txt | 69 - mailman/rules/docs/news-moderation.txt | 36 - mailman/rules/docs/no-subject.txt | 33 - mailman/rules/docs/recipients.txt | 40 - mailman/rules/docs/rules.txt | 69 - mailman/rules/docs/suspicious.txt | 35 - mailman/rules/docs/truth.txt | 9 - mailman/rules/emergency.py | 50 - mailman/rules/implicit_dest.py | 99 - mailman/rules/loop.py | 48 - mailman/rules/max_recipients.py | 52 - mailman/rules/max_size.py | 50 - mailman/rules/moderation.py | 68 - mailman/rules/news_moderation.py | 49 - mailman/rules/no_subject.py | 46 - mailman/rules/suspicious.py | 100 - mailman/rules/truth.py | 45 - mailman/styles/__init__.py | 0 mailman/styles/default.py | 250 - mailman/styles/manager.py | 94 - mailman/templates/__init__.py | 0 mailman/templates/en/__init__.py | 0 mailman/templates/en/adminaddrchgack.txt | 4 - mailman/templates/en/admindbdetails.html | 65 - mailman/templates/en/admindbpreamble.html | 10 - mailman/templates/en/admindbsummary.html | 14 - mailman/templates/en/adminsubscribeack.txt | 1 - mailman/templates/en/adminunsubscribeack.txt | 1 - mailman/templates/en/admlogin.html | 39 - mailman/templates/en/approve.txt | 15 - mailman/templates/en/archidxentry.html | 4 - mailman/templates/en/archidxfoot.html | 21 - mailman/templates/en/archidxhead.html | 24 - mailman/templates/en/archlistend.html | 1 - mailman/templates/en/archliststart.html | 4 - mailman/templates/en/archtoc.html | 20 - mailman/templates/en/archtocentry.html | 12 - mailman/templates/en/archtocnombox.html | 18 - mailman/templates/en/article.html | 50 - mailman/templates/en/bounce.txt | 13 - mailman/templates/en/checkdbs.txt | 7 - mailman/templates/en/convert.txt | 34 - mailman/templates/en/cronpass.txt | 19 - mailman/templates/en/disabled.txt | 25 - mailman/templates/en/emptyarchive.html | 15 - mailman/templates/en/headfoot.html | 28 - mailman/templates/en/help.txt | 33 - mailman/templates/en/invite.txt | 20 - mailman/templates/en/listinfo.html | 143 - mailman/templates/en/masthead.txt | 13 - mailman/templates/en/newlist.txt | 35 - mailman/templates/en/nomoretoday.txt | 8 - mailman/templates/en/options.html | 316 - mailman/templates/en/postack.txt | 8 - mailman/templates/en/postauth.txt | 13 - mailman/templates/en/postheld.txt | 15 - mailman/templates/en/private.html | 43 - mailman/templates/en/probe.txt | 25 - mailman/templates/en/refuse.txt | 13 - mailman/templates/en/roster.html | 52 - mailman/templates/en/subauth.txt | 11 - mailman/templates/en/subscribe.html | 8 - mailman/templates/en/subscribeack.txt | 25 - mailman/templates/en/unsub.txt | 23 - mailman/templates/en/unsubauth.txt | 11 - mailman/templates/en/userpass.txt | 24 - mailman/templates/en/verify.txt | 19 - mailman/testing/__init__.py | 0 mailman/testing/helpers.py | 248 - mailman/testing/layers.py | 204 - mailman/testing/mta.py | 46 - mailman/testing/smtplistener.py | 97 - mailman/testing/testing.cfg | 87 - mailman/tests/__init__.py | 0 mailman/tests/bounces/__init__.py | 0 mailman/tests/bounces/bounce_01.txt | 95 - mailman/tests/bounces/bounce_02.txt | 36 - mailman/tests/bounces/bounce_03.txt | 109 - mailman/tests/bounces/dsn_01.txt | 217 - mailman/tests/bounces/dsn_02.txt | 187 - mailman/tests/bounces/dsn_03.txt | 144 - mailman/tests/bounces/dsn_04.txt | 202 - mailman/tests/bounces/dsn_05.txt | 125 - mailman/tests/bounces/dsn_06.txt | 122 - mailman/tests/bounces/dsn_07.txt | 121 - mailman/tests/bounces/dsn_08.txt | 131 - mailman/tests/bounces/dsn_09.txt | 85 - mailman/tests/bounces/dsn_10.txt | 66 - mailman/tests/bounces/dsn_11.txt | 176 - mailman/tests/bounces/dsn_12.txt | 40 - mailman/tests/bounces/dsn_13.txt | 311 - mailman/tests/bounces/dsn_14.txt | 149 - mailman/tests/bounces/dsn_15.txt | 278 - mailman/tests/bounces/dumbass_01.txt | 109 - mailman/tests/bounces/exim_01.txt | 58 - mailman/tests/bounces/groupwise_01.txt | 151 - mailman/tests/bounces/groupwise_02.txt | 186 - mailman/tests/bounces/hotpop_01.txt | 180 - mailman/tests/bounces/llnl_01.txt | 203 - mailman/tests/bounces/microsoft_01.txt | 108 - mailman/tests/bounces/microsoft_02.txt | 119 - mailman/tests/bounces/microsoft_03.txt | 65 - mailman/tests/bounces/netscape_01.txt | 123 - mailman/tests/bounces/newmailru_01.txt | 112 - mailman/tests/bounces/postfix_01.txt | 123 - mailman/tests/bounces/postfix_02.txt | 60 - mailman/tests/bounces/postfix_03.txt | 145 - mailman/tests/bounces/postfix_04.txt | 240 - mailman/tests/bounces/postfix_05.txt | 231 - mailman/tests/bounces/qmail_01.txt | 103 - mailman/tests/bounces/qmail_02.txt | 73 - mailman/tests/bounces/qmail_03.txt | 245 - mailman/tests/bounces/qmail_04.txt | 81 - mailman/tests/bounces/qmail_05.txt | 121 - mailman/tests/bounces/sendmail_01.txt | 146 - mailman/tests/bounces/simple_01.txt | 153 - mailman/tests/bounces/simple_02.txt | 118 - mailman/tests/bounces/simple_03.txt | 68 - mailman/tests/bounces/simple_04.txt | 105 - mailman/tests/bounces/simple_05.txt | 81 - mailman/tests/bounces/simple_06.txt | 77 - mailman/tests/bounces/simple_07.txt | 21 - mailman/tests/bounces/simple_08.txt | 81 - mailman/tests/bounces/simple_09.txt | 27 - mailman/tests/bounces/simple_10.txt | 45 - mailman/tests/bounces/simple_11.txt | 68 - mailman/tests/bounces/simple_12.txt | 81 - mailman/tests/bounces/simple_13.txt | 60 - mailman/tests/bounces/simple_14.txt | 122 - mailman/tests/bounces/simple_15.txt | 259 - mailman/tests/bounces/simple_16.txt | 78 - mailman/tests/bounces/simple_17.txt | 76 - mailman/tests/bounces/simple_18.txt | 73 - mailman/tests/bounces/simple_19.txt | 77 - mailman/tests/bounces/simple_20.txt | 27 - mailman/tests/bounces/simple_21.txt | 46 - mailman/tests/bounces/simple_22.txt | 25 - mailman/tests/bounces/simple_23.txt | 152 - mailman/tests/bounces/simple_24.txt | 33 - mailman/tests/bounces/simple_25.txt | 379 - mailman/tests/bounces/simple_26.txt | 74 - mailman/tests/bounces/simple_27.txt | 279 - mailman/tests/bounces/sina_01.txt | 128 - mailman/tests/bounces/smtp32_01.txt | 97 - mailman/tests/bounces/smtp32_02.txt | 96 - mailman/tests/bounces/smtp32_03.txt | 92 - mailman/tests/bounces/smtp32_04.txt | 47 - mailman/tests/bounces/smtp32_05.txt | 63 - mailman/tests/bounces/smtp32_06.txt | 41 - mailman/tests/bounces/smtp32_07.txt | 81 - mailman/tests/bounces/yahoo_01.txt | 47 - mailman/tests/bounces/yahoo_02.txt | Bin 2212 -> 0 bytes mailman/tests/bounces/yahoo_03.txt | 98 - mailman/tests/bounces/yahoo_04.txt | 150 - mailman/tests/bounces/yahoo_05.txt | 150 - mailman/tests/bounces/yahoo_06.txt | 105 - mailman/tests/bounces/yahoo_07.txt | 112 - mailman/tests/bounces/yahoo_08.txt | 129 - mailman/tests/bounces/yahoo_09.txt | 165 - mailman/tests/bounces/yahoo_10.txt | 83 - mailman/tests/bounces/yale_01.txt | 422 - mailman/tests/test_bounces.py | 229 - mailman/tests/test_documentation.py | 150 - mailman/tests/test_membership.py | 392 - mailman/tests/test_passwords.py | 150 - mailman/tests/test_security_mgr.py | 241 - mailman/utilities/__init__.py | 0 mailman/utilities/filesystem.py | 78 - mailman/utilities/string.py | 59 - mailman/version.py | 48 - mailman/web/Cgi/Auth.py | 60 - mailman/web/Cgi/__init__.py | 0 mailman/web/Cgi/admin.py | 1433 -- mailman/web/Cgi/admindb.py | 813 - mailman/web/Cgi/confirm.py | 834 - mailman/web/Cgi/create.py | 400 - mailman/web/Cgi/edithtml.py | 175 - mailman/web/Cgi/listinfo.py | 207 - mailman/web/Cgi/options.py | 1000 -- mailman/web/Cgi/private.py | 190 - mailman/web/Cgi/rmlist.py | 243 - mailman/web/Cgi/roster.py | 130 - mailman/web/Cgi/subscribe.py | 252 - mailman/web/Cgi/wsgi_app.py | 286 - mailman/web/Gui/Archive.py | 45 - mailman/web/Gui/Autoresponse.py | 99 - mailman/web/Gui/Bounce.py | 195 - mailman/web/Gui/ContentFilter.py | 199 - mailman/web/Gui/Digest.py | 161 - mailman/web/Gui/GUIBase.py | 209 - mailman/web/Gui/General.py | 464 - mailman/web/Gui/Language.py | 128 - mailman/web/Gui/Membership.py | 34 - mailman/web/Gui/NonDigest.py | 158 - mailman/web/Gui/Passwords.py | 31 - mailman/web/Gui/Privacy.py | 537 - mailman/web/Gui/Topics.py | 162 - mailman/web/Gui/Usenet.py | 140 - mailman/web/Gui/__init__.py | 33 - mailman/web/HTMLFormatter.py | 437 - mailman/web/__init__.py | 0 mailman/web/htmlformat.py | 670 - setup.py | 7 +- src/mailman/Archiver/Archiver.py | 230 + src/mailman/Archiver/HyperArch.py | 1237 ++ src/mailman/Archiver/HyperDatabase.py | 339 + src/mailman/Archiver/__init__.py | 18 + src/mailman/Archiver/pipermail.py | 874 + src/mailman/Bouncers/BouncerAPI.py | 64 + src/mailman/Bouncers/Caiwireless.py | 45 + src/mailman/Bouncers/Compuserve.py | 46 + src/mailman/Bouncers/DSN.py | 99 + src/mailman/Bouncers/Exchange.py | 48 + src/mailman/Bouncers/Exim.py | 31 + src/mailman/Bouncers/GroupWise.py | 71 + src/mailman/Bouncers/LLNL.py | 32 + src/mailman/Bouncers/Microsoft.py | 53 + src/mailman/Bouncers/Netscape.py | 89 + src/mailman/Bouncers/Postfix.py | 86 + src/mailman/Bouncers/Qmail.py | 72 + src/mailman/Bouncers/SMTP32.py | 60 + src/mailman/Bouncers/SimpleMatch.py | 204 + src/mailman/Bouncers/SimpleWarning.py | 62 + src/mailman/Bouncers/Sina.py | 48 + src/mailman/Bouncers/Yahoo.py | 54 + src/mailman/Bouncers/Yale.py | 80 + src/mailman/Bouncers/__init__.py | 0 src/mailman/Mailbox.py | 106 + src/mailman/Message.py | 297 + src/mailman/Utils.py | 702 + src/mailman/__init__.py | 0 src/mailman/app/__init__.py | 0 src/mailman/app/bounces.py | 63 + src/mailman/app/commands.py | 44 + src/mailman/app/lifecycle.py | 114 + src/mailman/app/membership.py | 137 + src/mailman/app/moderator.py | 351 + src/mailman/app/notifications.py | 136 + src/mailman/app/registrar.py | 163 + src/mailman/app/replybot.py | 125 + src/mailman/archiving/__init__.py | 0 src/mailman/archiving/mailarchive.py | 87 + src/mailman/archiving/mhonarc.py | 97 + src/mailman/archiving/pipermail.py | 121 + src/mailman/archiving/prototype.py | 77 + src/mailman/attic/Bouncer.py | 250 + src/mailman/attic/Defaults.py | 1324 ++ src/mailman/attic/Deliverer.py | 174 + src/mailman/attic/Digester.py | 57 + src/mailman/attic/MailList.py | 731 + src/mailman/attic/SecurityManager.py | 306 + src/mailman/attic/bin/clone_member | 219 + src/mailman/attic/bin/discard | 120 + src/mailman/attic/bin/fix_url.py | 93 + src/mailman/attic/bin/list_admins | 101 + src/mailman/attic/bin/msgfmt.py | 203 + src/mailman/attic/bin/po2templ.py | 90 + src/mailman/attic/bin/pygettext.py | 545 + src/mailman/attic/bin/remove_members | 186 + src/mailman/attic/bin/reset_pw.py | 83 + src/mailman/attic/bin/sync_members | 286 + src/mailman/attic/bin/templ2pot.py | 120 + src/mailman/attic/bin/transcheck | 412 + src/mailman/bin/__init__.py | 61 + src/mailman/bin/add_members.py | 186 + src/mailman/bin/arch.py | 151 + src/mailman/bin/bumpdigests.py | 74 + src/mailman/bin/check_perms.py | 408 + src/mailman/bin/checkdbs.py | 199 + src/mailman/bin/cleanarch.py | 133 + src/mailman/bin/config_list.py | 332 + src/mailman/bin/create_list.py | 129 + src/mailman/bin/disabled.py | 201 + src/mailman/bin/docs/master.txt | 49 + src/mailman/bin/dumpdb.py | 88 + src/mailman/bin/export.py | 310 + src/mailman/bin/find_member.py | 135 + src/mailman/bin/gate_news.py | 243 + src/mailman/bin/genaliases.py | 64 + src/mailman/bin/import.py | 315 + src/mailman/bin/inject.py | 89 + src/mailman/bin/list_lists.py | 104 + src/mailman/bin/list_members.py | 201 + src/mailman/bin/list_owners.py | 88 + src/mailman/bin/mailmanctl.py | 232 + src/mailman/bin/master.py | 452 + src/mailman/bin/mmsitepass.py | 113 + src/mailman/bin/nightly_gzip.py | 117 + src/mailman/bin/qrunner.py | 269 + src/mailman/bin/remove_list.py | 83 + src/mailman/bin/senddigests.py | 83 + src/mailman/bin/set_members.py | 189 + src/mailman/bin/show_config.py | 97 + src/mailman/bin/show_qfiles.py | 91 + src/mailman/bin/unshunt.py | 51 + src/mailman/bin/update.py | 660 + src/mailman/bin/version.py | 46 + src/mailman/bin/withlist.py | 220 + src/mailman/chains/__init__.py | 0 src/mailman/chains/accept.py | 58 + src/mailman/chains/base.py | 122 + src/mailman/chains/builtin.py | 86 + src/mailman/chains/discard.py | 47 + src/mailman/chains/headers.py | 156 + src/mailman/chains/hold.py | 178 + src/mailman/chains/reject.py | 59 + src/mailman/commands/__init__.py | 22 + src/mailman/commands/cmd_confirm.py | 98 + src/mailman/commands/cmd_help.py | 93 + src/mailman/commands/cmd_info.py | 50 + src/mailman/commands/cmd_leave.py | 21 + src/mailman/commands/cmd_lists.py | 65 + src/mailman/commands/cmd_password.py | 123 + src/mailman/commands/cmd_remove.py | 21 + src/mailman/commands/cmd_set.py | 360 + src/mailman/commands/cmd_unsubscribe.py | 88 + src/mailman/commands/cmd_who.py | 152 + src/mailman/commands/docs/echo.txt | 30 + src/mailman/commands/docs/end.txt | 37 + src/mailman/commands/docs/join.txt | 170 + src/mailman/commands/echo.py | 48 + src/mailman/commands/end.py | 51 + src/mailman/commands/join.py | 126 + src/mailman/config/__init__.py | 30 + src/mailman/config/config.py | 206 + src/mailman/config/mailman.cfg | 69 + src/mailman/config/schema.cfg | 589 + src/mailman/constants.py | 44 + src/mailman/core/__init__.py | 0 src/mailman/core/chains.py | 118 + src/mailman/core/errors.py | 172 + src/mailman/core/initialize.py | 125 + src/mailman/core/logging.py | 163 + src/mailman/core/pipelines.py | 125 + src/mailman/core/plugins.py | 74 + src/mailman/core/rules.py | 46 + src/mailman/database/__init__.py | 153 + src/mailman/database/address.py | 96 + src/mailman/database/language.py | 40 + src/mailman/database/listmanager.py | 82 + src/mailman/database/mailinglist.py | 272 + src/mailman/database/mailman.sql | 208 + src/mailman/database/member.py | 105 + src/mailman/database/message.py | 53 + src/mailman/database/messagestore.py | 137 + src/mailman/database/model.py | 56 + src/mailman/database/pending.py | 177 + src/mailman/database/preferences.py | 50 + src/mailman/database/requests.py | 138 + src/mailman/database/roster.py | 270 + src/mailman/database/transaction.py | 53 + src/mailman/database/types.py | 64 + src/mailman/database/user.py | 94 + src/mailman/database/usermanager.py | 103 + src/mailman/database/version.py | 40 + src/mailman/docs/__init__.py | 0 src/mailman/docs/addresses.txt | 231 + src/mailman/docs/archivers.txt | 184 + src/mailman/docs/bounces.txt | 107 + src/mailman/docs/chains.txt | 345 + src/mailman/docs/domains.txt | 46 + src/mailman/docs/languages.txt | 104 + src/mailman/docs/lifecycle.txt | 136 + src/mailman/docs/listmanager.txt | 88 + src/mailman/docs/membership.txt | 230 + src/mailman/docs/message.txt | 48 + src/mailman/docs/messagestore.txt | 113 + src/mailman/docs/mlist-addresses.txt | 76 + src/mailman/docs/pending.txt | 94 + src/mailman/docs/pipelines.txt | 186 + src/mailman/docs/registration.txt | 362 + src/mailman/docs/requests.txt | 883 + src/mailman/docs/styles.txt | 156 + src/mailman/docs/usermanager.txt | 124 + src/mailman/docs/users.txt | 195 + src/mailman/domain.py | 70 + src/mailman/extras/__init__.py | 0 src/mailman/extras/mailman.cfg.in | 46 + src/mailman/i18n.py | 198 + src/mailman/inject.py | 86 + src/mailman/interact.py | 75 + src/mailman/interfaces/__init__.py | 44 + src/mailman/interfaces/address.py | 101 + src/mailman/interfaces/archiver.py | 78 + src/mailman/interfaces/chain.py | 110 + src/mailman/interfaces/command.py | 68 + src/mailman/interfaces/database.py | 97 + src/mailman/interfaces/domain.py | 85 + src/mailman/interfaces/errors.py | 35 + src/mailman/interfaces/handler.py | 45 + src/mailman/interfaces/languages.py | 89 + src/mailman/interfaces/listmanager.py | 84 + src/mailman/interfaces/mailinglist.py | 273 + src/mailman/interfaces/member.py | 187 + src/mailman/interfaces/messages.py | 111 + src/mailman/interfaces/mlistrequest.py | 37 + src/mailman/interfaces/mta.py | 42 + src/mailman/interfaces/pending.py | 99 + src/mailman/interfaces/permissions.py | 36 + src/mailman/interfaces/pipeline.py | 40 + src/mailman/interfaces/preferences.py | 77 + src/mailman/interfaces/registrar.py | 83 + src/mailman/interfaces/requests.py | 115 + src/mailman/interfaces/roster.py | 61 + src/mailman/interfaces/rules.py | 53 + src/mailman/interfaces/runner.py | 119 + src/mailman/interfaces/styles.py | 120 + src/mailman/interfaces/switchboard.py | 90 + src/mailman/interfaces/user.py | 84 + src/mailman/interfaces/usermanager.py | 96 + src/mailman/languages.py | 67 + src/mailman/messages/__init__.py | 0 src/mailman/messages/ar/LC_MESSAGES/mailman.po | 15702 +++++++++++++++++ src/mailman/messages/ca/LC_MESSAGES/mailman.po | 15421 +++++++++++++++++ src/mailman/messages/cs/LC_MESSAGES/mailman.po | 13393 +++++++++++++++ src/mailman/messages/da/LC_MESSAGES/mailman.po | 15972 +++++++++++++++++ src/mailman/messages/de/LC_MESSAGES/mailman.po | 15318 +++++++++++++++++ src/mailman/messages/de/README.de | 21 + src/mailman/messages/docstring.files | 59 + src/mailman/messages/es/LC_MESSAGES/mailman.po | 16349 ++++++++++++++++++ src/mailman/messages/es/README.es | 82 + src/mailman/messages/et/LC_MESSAGES/mailman.po | 14338 ++++++++++++++++ src/mailman/messages/eu/LC_MESSAGES/mailman.po | 14145 +++++++++++++++ src/mailman/messages/eu/README.eu | 103 + src/mailman/messages/fi/LC_MESSAGES/mailman.po | 13862 +++++++++++++++ src/mailman/messages/fi/README.fi | 13 + src/mailman/messages/fr/LC_MESSAGES/mailman.po | 15420 +++++++++++++++++ src/mailman/messages/fr/README.fr | 7 + src/mailman/messages/hr/LC_MESSAGES/mailman.po | 13737 +++++++++++++++ src/mailman/messages/hu/FAQ.hu | 464 + src/mailman/messages/hu/INSTALL.hu | 640 + src/mailman/messages/hu/LC_MESSAGES/mailman.po | 14882 ++++++++++++++++ src/mailman/messages/hu/README.BSD.hu | 28 + src/mailman/messages/hu/README.CONTRIB.hu | 17 + src/mailman/messages/hu/README.EXIM.hu | 359 + src/mailman/messages/hu/README.LINUX.hu | 59 + src/mailman/messages/hu/README.MACOSX.hu | 31 + src/mailman/messages/hu/README.NETSCAPE.hu | 57 + src/mailman/messages/hu/README.POSTFIX.hu | 239 + src/mailman/messages/hu/README.QMAIL.hu | 186 + src/mailman/messages/hu/README.SENDMAIL.hu | 86 + src/mailman/messages/hu/README.USERAGENT.hu | 49 + src/mailman/messages/hu/README.hu | 271 + src/mailman/messages/hu/UPGRADING.hu | 391 + src/mailman/messages/ia/LC_MESSAGES/mailman.po | 14426 ++++++++++++++++ src/mailman/messages/it/LC_MESSAGES/mailman.po | 15623 +++++++++++++++++ src/mailman/messages/it/README.it | 32 + src/mailman/messages/ja/INSTALL | 615 + src/mailman/messages/ja/LC_MESSAGES/mailman.po | 14328 ++++++++++++++++ src/mailman/messages/ja/README | 214 + src/mailman/messages/ja/README.ja | 109 + src/mailman/messages/ja/UPGRADING | 215 + src/mailman/messages/ja/doc/Defaults.py.in | 1442 ++ src/mailman/messages/ja/doc/mailman-install.tex | 1933 +++ src/mailman/messages/ja/doc/mailman-member.tex | 1787 ++ src/mailman/messages/ko/LC_MESSAGES/mailman.po | 12970 ++++++++++++++ src/mailman/messages/ko/README.ko | 26 + src/mailman/messages/lt/LC_MESSAGES/mailman.po | 12116 +++++++++++++ src/mailman/messages/mailman.pot | 10031 +++++++++++ src/mailman/messages/marked.files | 130 + src/mailman/messages/nl/LC_MESSAGES/mailman.po | 13629 +++++++++++++++ src/mailman/messages/no/LC_MESSAGES/mailman.po | 15503 +++++++++++++++++ src/mailman/messages/pl/LC_MESSAGES/mailman.po | 13120 ++++++++++++++ src/mailman/messages/pl/README.pl | 28 + src/mailman/messages/pt/LC_MESSAGES/mailman.po | 14924 ++++++++++++++++ src/mailman/messages/pt_BR/LC_MESSAGES/mailman.po | 15033 ++++++++++++++++ src/mailman/messages/ro/LC_MESSAGES/mailman.po | 14633 ++++++++++++++++ src/mailman/messages/ru/LC_MESSAGES/mailman.po | 14862 ++++++++++++++++ src/mailman/messages/ru/README.ru | 17 + src/mailman/messages/sl/LC_MESSAGES/mailman.po | 17650 +++++++++++++++++++ src/mailman/messages/sr/LC_MESSAGES/mailman.po | 11873 +++++++++++++ src/mailman/messages/sr/readme.sr | 6 + src/mailman/messages/sv/LC_MESSAGES/mailman.po | 18097 ++++++++++++++++++++ src/mailman/messages/sv/README.sv | 30 + src/mailman/messages/tr/LC_MESSAGES/mailman.po | 13641 +++++++++++++++ src/mailman/messages/uk/LC_MESSAGES/mailman.po | 14897 ++++++++++++++++ src/mailman/messages/vi/LC_MESSAGES/mailman.po | 14584 ++++++++++++++++ src/mailman/messages/zh_CN/LC_MESSAGES/mailman.po | 14142 +++++++++++++++ src/mailman/messages/zh_TW/LC_MESSAGES/mailman.po | 12964 ++++++++++++++ src/mailman/mta/__init__.py | 0 src/mailman/mta/null.py | 51 + src/mailman/mta/postfix.py | 123 + src/mailman/mta/smtp_direct.py | 417 + src/mailman/options.py | 143 + src/mailman/passwords.py | 253 + src/mailman/pipeline/__init__.py | 54 + src/mailman/pipeline/acknowledge.py | 80 + src/mailman/pipeline/after_delivery.py | 48 + src/mailman/pipeline/avoid_duplicates.py | 116 + src/mailman/pipeline/calculate_recipients.py | 148 + src/mailman/pipeline/cleanse.py | 75 + src/mailman/pipeline/cleanse_dkim.py | 58 + src/mailman/pipeline/cook_headers.py | 357 + src/mailman/pipeline/decorate.py | 231 + src/mailman/pipeline/docs/ack-headers.txt | 40 + src/mailman/pipeline/docs/acknowledge.txt | 159 + src/mailman/pipeline/docs/after-delivery.txt | 27 + src/mailman/pipeline/docs/archives.txt | 133 + src/mailman/pipeline/docs/avoid-duplicates.txt | 168 + src/mailman/pipeline/docs/calc-recips.txt | 100 + src/mailman/pipeline/docs/cleanse.txt | 94 + src/mailman/pipeline/docs/cook-headers.txt | 326 + src/mailman/pipeline/docs/decorate.txt | 317 + src/mailman/pipeline/docs/digests.txt | 535 + src/mailman/pipeline/docs/file-recips.txt | 96 + src/mailman/pipeline/docs/filtering.txt | 340 + src/mailman/pipeline/docs/nntp.txt | 65 + src/mailman/pipeline/docs/reply-to.txt | 127 + src/mailman/pipeline/docs/replybot.txt | 213 + src/mailman/pipeline/docs/scrubber.txt | 225 + src/mailman/pipeline/docs/subject-munging.txt | 244 + src/mailman/pipeline/docs/tagger.txt | 235 + src/mailman/pipeline/docs/to-outgoing.txt | 173 + src/mailman/pipeline/file_recipients.py | 65 + src/mailman/pipeline/mime_delete.py | 285 + src/mailman/pipeline/moderate.py | 175 + src/mailman/pipeline/owner_recipients.py | 34 + src/mailman/pipeline/replybot.py | 134 + src/mailman/pipeline/scrubber.py | 509 + src/mailman/pipeline/tagger.py | 187 + src/mailman/pipeline/to_archive.py | 55 + src/mailman/pipeline/to_digest.py | 440 + src/mailman/pipeline/to_outgoing.py | 78 + src/mailman/pipeline/to_usenet.py | 69 + src/mailman/queue/__init__.py | 466 + src/mailman/queue/archive.py | 91 + src/mailman/queue/bounce.py | 316 + src/mailman/queue/command.py | 214 + src/mailman/queue/docs/OVERVIEW.txt | 78 + src/mailman/queue/docs/archiver.txt | 34 + src/mailman/queue/docs/command.txt | 170 + src/mailman/queue/docs/incoming.txt | 200 + src/mailman/queue/docs/lmtp.txt | 103 + src/mailman/queue/docs/news.txt | 157 + src/mailman/queue/docs/outgoing.txt | 75 + src/mailman/queue/docs/runner.txt | 72 + src/mailman/queue/docs/switchboard.txt | 182 + src/mailman/queue/http.py | 73 + src/mailman/queue/incoming.py | 43 + src/mailman/queue/lmtp.py | 218 + src/mailman/queue/maildir.py | 190 + src/mailman/queue/news.py | 166 + src/mailman/queue/outgoing.py | 130 + src/mailman/queue/pipeline.py | 35 + src/mailman/queue/retry.py | 37 + src/mailman/queue/virgin.py | 39 + src/mailman/rules/__init__.py | 54 + src/mailman/rules/administrivia.py | 102 + src/mailman/rules/any.py | 45 + src/mailman/rules/approved.py | 121 + src/mailman/rules/docs/administrivia.txt | 99 + src/mailman/rules/docs/approve.txt | 472 + src/mailman/rules/docs/emergency.txt | 72 + src/mailman/rules/docs/header-matching.txt | 144 + src/mailman/rules/docs/implicit-dest.txt | 75 + src/mailman/rules/docs/loop.txt | 48 + src/mailman/rules/docs/max-size.txt | 39 + src/mailman/rules/docs/moderation.txt | 69 + src/mailman/rules/docs/news-moderation.txt | 36 + src/mailman/rules/docs/no-subject.txt | 33 + src/mailman/rules/docs/recipients.txt | 40 + src/mailman/rules/docs/rules.txt | 69 + src/mailman/rules/docs/suspicious.txt | 35 + src/mailman/rules/docs/truth.txt | 9 + src/mailman/rules/emergency.py | 50 + src/mailman/rules/implicit_dest.py | 99 + src/mailman/rules/loop.py | 48 + src/mailman/rules/max_recipients.py | 52 + src/mailman/rules/max_size.py | 50 + src/mailman/rules/moderation.py | 68 + src/mailman/rules/news_moderation.py | 49 + src/mailman/rules/no_subject.py | 46 + src/mailman/rules/suspicious.py | 100 + src/mailman/rules/truth.py | 45 + src/mailman/styles/__init__.py | 0 src/mailman/styles/default.py | 250 + src/mailman/styles/manager.py | 94 + src/mailman/templates/__init__.py | 0 src/mailman/templates/en/__init__.py | 0 src/mailman/templates/en/adminaddrchgack.txt | 4 + src/mailman/templates/en/admindbdetails.html | 65 + src/mailman/templates/en/admindbpreamble.html | 10 + src/mailman/templates/en/admindbsummary.html | 14 + src/mailman/templates/en/adminsubscribeack.txt | 1 + src/mailman/templates/en/adminunsubscribeack.txt | 1 + src/mailman/templates/en/admlogin.html | 39 + src/mailman/templates/en/approve.txt | 15 + src/mailman/templates/en/archidxentry.html | 4 + src/mailman/templates/en/archidxfoot.html | 21 + src/mailman/templates/en/archidxhead.html | 24 + src/mailman/templates/en/archlistend.html | 1 + src/mailman/templates/en/archliststart.html | 4 + src/mailman/templates/en/archtoc.html | 20 + src/mailman/templates/en/archtocentry.html | 12 + src/mailman/templates/en/archtocnombox.html | 18 + src/mailman/templates/en/article.html | 50 + src/mailman/templates/en/bounce.txt | 13 + src/mailman/templates/en/checkdbs.txt | 7 + src/mailman/templates/en/convert.txt | 34 + src/mailman/templates/en/cronpass.txt | 19 + src/mailman/templates/en/disabled.txt | 25 + src/mailman/templates/en/emptyarchive.html | 15 + src/mailman/templates/en/headfoot.html | 28 + src/mailman/templates/en/help.txt | 33 + src/mailman/templates/en/invite.txt | 20 + src/mailman/templates/en/listinfo.html | 143 + src/mailman/templates/en/masthead.txt | 13 + src/mailman/templates/en/newlist.txt | 35 + src/mailman/templates/en/nomoretoday.txt | 8 + src/mailman/templates/en/options.html | 316 + src/mailman/templates/en/postack.txt | 8 + src/mailman/templates/en/postauth.txt | 13 + src/mailman/templates/en/postheld.txt | 15 + src/mailman/templates/en/private.html | 43 + src/mailman/templates/en/probe.txt | 25 + src/mailman/templates/en/refuse.txt | 13 + src/mailman/templates/en/roster.html | 52 + src/mailman/templates/en/subauth.txt | 11 + src/mailman/templates/en/subscribe.html | 8 + src/mailman/templates/en/subscribeack.txt | 25 + src/mailman/templates/en/unsub.txt | 23 + src/mailman/templates/en/unsubauth.txt | 11 + src/mailman/templates/en/userpass.txt | 24 + src/mailman/templates/en/verify.txt | 19 + src/mailman/testing/__init__.py | 0 src/mailman/testing/helpers.py | 248 + src/mailman/testing/layers.py | 204 + src/mailman/testing/mta.py | 46 + src/mailman/testing/smtplistener.py | 97 + src/mailman/testing/testing.cfg | 87 + src/mailman/tests/__init__.py | 0 src/mailman/tests/bounces/__init__.py | 0 src/mailman/tests/bounces/bounce_01.txt | 95 + src/mailman/tests/bounces/bounce_02.txt | 36 + src/mailman/tests/bounces/bounce_03.txt | 109 + src/mailman/tests/bounces/dsn_01.txt | 217 + src/mailman/tests/bounces/dsn_02.txt | 187 + src/mailman/tests/bounces/dsn_03.txt | 144 + src/mailman/tests/bounces/dsn_04.txt | 202 + src/mailman/tests/bounces/dsn_05.txt | 125 + src/mailman/tests/bounces/dsn_06.txt | 122 + src/mailman/tests/bounces/dsn_07.txt | 121 + src/mailman/tests/bounces/dsn_08.txt | 131 + src/mailman/tests/bounces/dsn_09.txt | 85 + src/mailman/tests/bounces/dsn_10.txt | 66 + src/mailman/tests/bounces/dsn_11.txt | 176 + src/mailman/tests/bounces/dsn_12.txt | 40 + src/mailman/tests/bounces/dsn_13.txt | 311 + src/mailman/tests/bounces/dsn_14.txt | 149 + src/mailman/tests/bounces/dsn_15.txt | 278 + src/mailman/tests/bounces/dumbass_01.txt | 109 + src/mailman/tests/bounces/exim_01.txt | 58 + src/mailman/tests/bounces/groupwise_01.txt | 151 + src/mailman/tests/bounces/groupwise_02.txt | 186 + src/mailman/tests/bounces/hotpop_01.txt | 180 + src/mailman/tests/bounces/llnl_01.txt | 203 + src/mailman/tests/bounces/microsoft_01.txt | 108 + src/mailman/tests/bounces/microsoft_02.txt | 119 + src/mailman/tests/bounces/microsoft_03.txt | 65 + src/mailman/tests/bounces/netscape_01.txt | 123 + src/mailman/tests/bounces/newmailru_01.txt | 112 + src/mailman/tests/bounces/postfix_01.txt | 123 + src/mailman/tests/bounces/postfix_02.txt | 60 + src/mailman/tests/bounces/postfix_03.txt | 145 + src/mailman/tests/bounces/postfix_04.txt | 240 + src/mailman/tests/bounces/postfix_05.txt | 231 + src/mailman/tests/bounces/qmail_01.txt | 103 + src/mailman/tests/bounces/qmail_02.txt | 73 + src/mailman/tests/bounces/qmail_03.txt | 245 + src/mailman/tests/bounces/qmail_04.txt | 81 + src/mailman/tests/bounces/qmail_05.txt | 121 + src/mailman/tests/bounces/sendmail_01.txt | 146 + src/mailman/tests/bounces/simple_01.txt | 153 + src/mailman/tests/bounces/simple_02.txt | 118 + src/mailman/tests/bounces/simple_03.txt | 68 + src/mailman/tests/bounces/simple_04.txt | 105 + src/mailman/tests/bounces/simple_05.txt | 81 + src/mailman/tests/bounces/simple_06.txt | 77 + src/mailman/tests/bounces/simple_07.txt | 21 + src/mailman/tests/bounces/simple_08.txt | 81 + src/mailman/tests/bounces/simple_09.txt | 27 + src/mailman/tests/bounces/simple_10.txt | 45 + src/mailman/tests/bounces/simple_11.txt | 68 + src/mailman/tests/bounces/simple_12.txt | 81 + src/mailman/tests/bounces/simple_13.txt | 60 + src/mailman/tests/bounces/simple_14.txt | 122 + src/mailman/tests/bounces/simple_15.txt | 259 + src/mailman/tests/bounces/simple_16.txt | 78 + src/mailman/tests/bounces/simple_17.txt | 76 + src/mailman/tests/bounces/simple_18.txt | 73 + src/mailman/tests/bounces/simple_19.txt | 77 + src/mailman/tests/bounces/simple_20.txt | 27 + src/mailman/tests/bounces/simple_21.txt | 46 + src/mailman/tests/bounces/simple_22.txt | 25 + src/mailman/tests/bounces/simple_23.txt | 152 + src/mailman/tests/bounces/simple_24.txt | 33 + src/mailman/tests/bounces/simple_25.txt | 379 + src/mailman/tests/bounces/simple_26.txt | 74 + src/mailman/tests/bounces/simple_27.txt | 279 + src/mailman/tests/bounces/sina_01.txt | 128 + src/mailman/tests/bounces/smtp32_01.txt | 97 + src/mailman/tests/bounces/smtp32_02.txt | 96 + src/mailman/tests/bounces/smtp32_03.txt | 92 + src/mailman/tests/bounces/smtp32_04.txt | 47 + src/mailman/tests/bounces/smtp32_05.txt | 63 + src/mailman/tests/bounces/smtp32_06.txt | 41 + src/mailman/tests/bounces/smtp32_07.txt | 81 + src/mailman/tests/bounces/yahoo_01.txt | 47 + src/mailman/tests/bounces/yahoo_02.txt | Bin 0 -> 2212 bytes src/mailman/tests/bounces/yahoo_03.txt | 98 + src/mailman/tests/bounces/yahoo_04.txt | 150 + src/mailman/tests/bounces/yahoo_05.txt | 150 + src/mailman/tests/bounces/yahoo_06.txt | 105 + src/mailman/tests/bounces/yahoo_07.txt | 112 + src/mailman/tests/bounces/yahoo_08.txt | 129 + src/mailman/tests/bounces/yahoo_09.txt | 165 + src/mailman/tests/bounces/yahoo_10.txt | 83 + src/mailman/tests/bounces/yale_01.txt | 422 + src/mailman/tests/test_bounces.py | 229 + src/mailman/tests/test_documentation.py | 150 + src/mailman/tests/test_membership.py | 392 + src/mailman/tests/test_passwords.py | 150 + src/mailman/tests/test_security_mgr.py | 241 + src/mailman/utilities/__init__.py | 0 src/mailman/utilities/filesystem.py | 78 + src/mailman/utilities/string.py | 59 + src/mailman/version.py | 48 + src/mailman/web/Cgi/Auth.py | 60 + src/mailman/web/Cgi/__init__.py | 0 src/mailman/web/Cgi/admin.py | 1433 ++ src/mailman/web/Cgi/admindb.py | 813 + src/mailman/web/Cgi/confirm.py | 834 + src/mailman/web/Cgi/create.py | 400 + src/mailman/web/Cgi/edithtml.py | 175 + src/mailman/web/Cgi/listinfo.py | 207 + src/mailman/web/Cgi/options.py | 1000 ++ src/mailman/web/Cgi/private.py | 190 + src/mailman/web/Cgi/rmlist.py | 243 + src/mailman/web/Cgi/roster.py | 130 + src/mailman/web/Cgi/subscribe.py | 252 + src/mailman/web/Cgi/wsgi_app.py | 286 + src/mailman/web/Gui/Archive.py | 45 + src/mailman/web/Gui/Autoresponse.py | 99 + src/mailman/web/Gui/Bounce.py | 195 + src/mailman/web/Gui/ContentFilter.py | 199 + src/mailman/web/Gui/Digest.py | 161 + src/mailman/web/Gui/GUIBase.py | 209 + src/mailman/web/Gui/General.py | 464 + src/mailman/web/Gui/Language.py | 128 + src/mailman/web/Gui/Membership.py | 34 + src/mailman/web/Gui/NonDigest.py | 158 + src/mailman/web/Gui/Passwords.py | 31 + src/mailman/web/Gui/Privacy.py | 537 + src/mailman/web/Gui/Topics.py | 162 + src/mailman/web/Gui/Usenet.py | 140 + src/mailman/web/Gui/__init__.py | 33 + src/mailman/web/HTMLFormatter.py | 437 + src/mailman/web/__init__.py | 0 src/mailman/web/htmlformat.py | 670 + 1114 files changed, 555311 insertions(+), 555307 deletions(-) delete mode 100644 mailman/Archiver/Archiver.py delete mode 100644 mailman/Archiver/HyperArch.py delete mode 100644 mailman/Archiver/HyperDatabase.py delete mode 100644 mailman/Archiver/__init__.py delete mode 100644 mailman/Archiver/pipermail.py delete mode 100644 mailman/Bouncers/BouncerAPI.py delete mode 100644 mailman/Bouncers/Caiwireless.py delete mode 100644 mailman/Bouncers/Compuserve.py delete mode 100644 mailman/Bouncers/DSN.py delete mode 100644 mailman/Bouncers/Exchange.py delete mode 100644 mailman/Bouncers/Exim.py delete mode 100644 mailman/Bouncers/GroupWise.py delete mode 100644 mailman/Bouncers/LLNL.py delete mode 100644 mailman/Bouncers/Microsoft.py delete mode 100644 mailman/Bouncers/Netscape.py delete mode 100644 mailman/Bouncers/Postfix.py delete mode 100644 mailman/Bouncers/Qmail.py delete mode 100644 mailman/Bouncers/SMTP32.py delete mode 100644 mailman/Bouncers/SimpleMatch.py delete mode 100644 mailman/Bouncers/SimpleWarning.py delete mode 100644 mailman/Bouncers/Sina.py delete mode 100644 mailman/Bouncers/Yahoo.py delete mode 100644 mailman/Bouncers/Yale.py delete mode 100644 mailman/Bouncers/__init__.py delete mode 100644 mailman/Mailbox.py delete mode 100644 mailman/Message.py delete mode 100644 mailman/Utils.py delete mode 100644 mailman/__init__.py delete mode 100644 mailman/app/__init__.py delete mode 100644 mailman/app/bounces.py delete mode 100644 mailman/app/commands.py delete mode 100644 mailman/app/lifecycle.py delete mode 100644 mailman/app/membership.py delete mode 100644 mailman/app/moderator.py delete mode 100644 mailman/app/notifications.py delete mode 100644 mailman/app/registrar.py delete mode 100644 mailman/app/replybot.py delete mode 100644 mailman/archiving/__init__.py delete mode 100644 mailman/archiving/mailarchive.py delete mode 100644 mailman/archiving/mhonarc.py delete mode 100644 mailman/archiving/pipermail.py delete mode 100644 mailman/archiving/prototype.py delete mode 100644 mailman/attic/Bouncer.py delete mode 100644 mailman/attic/Defaults.py delete mode 100644 mailman/attic/Deliverer.py delete mode 100644 mailman/attic/Digester.py delete mode 100644 mailman/attic/MailList.py delete mode 100644 mailman/attic/SecurityManager.py delete mode 100755 mailman/attic/bin/clone_member delete mode 100644 mailman/attic/bin/discard delete mode 100644 mailman/attic/bin/fix_url.py delete mode 100644 mailman/attic/bin/list_admins delete mode 100644 mailman/attic/bin/msgfmt.py delete mode 100644 mailman/attic/bin/po2templ.py delete mode 100644 mailman/attic/bin/pygettext.py delete mode 100755 mailman/attic/bin/remove_members delete mode 100644 mailman/attic/bin/reset_pw.py delete mode 100755 mailman/attic/bin/sync_members delete mode 100644 mailman/attic/bin/templ2pot.py delete mode 100755 mailman/attic/bin/transcheck delete mode 100644 mailman/bin/__init__.py delete mode 100644 mailman/bin/add_members.py delete mode 100644 mailman/bin/arch.py delete mode 100644 mailman/bin/bumpdigests.py delete mode 100644 mailman/bin/check_perms.py delete mode 100644 mailman/bin/checkdbs.py delete mode 100644 mailman/bin/cleanarch.py delete mode 100644 mailman/bin/config_list.py delete mode 100644 mailman/bin/create_list.py delete mode 100644 mailman/bin/disabled.py delete mode 100644 mailman/bin/docs/master.txt delete mode 100644 mailman/bin/dumpdb.py delete mode 100644 mailman/bin/export.py delete mode 100644 mailman/bin/find_member.py delete mode 100644 mailman/bin/gate_news.py delete mode 100644 mailman/bin/genaliases.py delete mode 100644 mailman/bin/import.py delete mode 100644 mailman/bin/inject.py delete mode 100644 mailman/bin/list_lists.py delete mode 100644 mailman/bin/list_members.py delete mode 100644 mailman/bin/list_owners.py delete mode 100644 mailman/bin/mailmanctl.py delete mode 100644 mailman/bin/master.py delete mode 100644 mailman/bin/mmsitepass.py delete mode 100644 mailman/bin/nightly_gzip.py delete mode 100644 mailman/bin/qrunner.py delete mode 100644 mailman/bin/remove_list.py delete mode 100644 mailman/bin/senddigests.py delete mode 100644 mailman/bin/set_members.py delete mode 100644 mailman/bin/show_config.py delete mode 100644 mailman/bin/show_qfiles.py delete mode 100644 mailman/bin/unshunt.py delete mode 100644 mailman/bin/update.py delete mode 100644 mailman/bin/version.py delete mode 100644 mailman/bin/withlist.py delete mode 100644 mailman/chains/__init__.py delete mode 100644 mailman/chains/accept.py delete mode 100644 mailman/chains/base.py delete mode 100644 mailman/chains/builtin.py delete mode 100644 mailman/chains/discard.py delete mode 100644 mailman/chains/headers.py delete mode 100644 mailman/chains/hold.py delete mode 100644 mailman/chains/reject.py delete mode 100644 mailman/commands/__init__.py delete mode 100644 mailman/commands/cmd_confirm.py delete mode 100644 mailman/commands/cmd_help.py delete mode 100644 mailman/commands/cmd_info.py delete mode 100644 mailman/commands/cmd_leave.py delete mode 100644 mailman/commands/cmd_lists.py delete mode 100644 mailman/commands/cmd_password.py delete mode 100644 mailman/commands/cmd_remove.py delete mode 100644 mailman/commands/cmd_set.py delete mode 100644 mailman/commands/cmd_unsubscribe.py delete mode 100644 mailman/commands/cmd_who.py delete mode 100644 mailman/commands/docs/echo.txt delete mode 100644 mailman/commands/docs/end.txt delete mode 100644 mailman/commands/docs/join.txt delete mode 100644 mailman/commands/echo.py delete mode 100644 mailman/commands/end.py delete mode 100644 mailman/commands/join.py delete mode 100644 mailman/config/__init__.py delete mode 100644 mailman/config/config.py delete mode 100644 mailman/config/mailman.cfg delete mode 100644 mailman/config/schema.cfg delete mode 100644 mailman/constants.py delete mode 100644 mailman/core/__init__.py delete mode 100644 mailman/core/chains.py delete mode 100644 mailman/core/errors.py delete mode 100644 mailman/core/initialize.py delete mode 100644 mailman/core/logging.py delete mode 100644 mailman/core/pipelines.py delete mode 100644 mailman/core/plugins.py delete mode 100644 mailman/core/rules.py delete mode 100644 mailman/database/__init__.py delete mode 100644 mailman/database/address.py delete mode 100644 mailman/database/language.py delete mode 100644 mailman/database/listmanager.py delete mode 100644 mailman/database/mailinglist.py delete mode 100644 mailman/database/mailman.sql delete mode 100644 mailman/database/member.py delete mode 100644 mailman/database/message.py delete mode 100644 mailman/database/messagestore.py delete mode 100644 mailman/database/model.py delete mode 100644 mailman/database/pending.py delete mode 100644 mailman/database/preferences.py delete mode 100644 mailman/database/requests.py delete mode 100644 mailman/database/roster.py delete mode 100644 mailman/database/transaction.py delete mode 100644 mailman/database/types.py delete mode 100644 mailman/database/user.py delete mode 100644 mailman/database/usermanager.py delete mode 100644 mailman/database/version.py delete mode 100644 mailman/docs/__init__.py delete mode 100644 mailman/docs/addresses.txt delete mode 100644 mailman/docs/archivers.txt delete mode 100644 mailman/docs/bounces.txt delete mode 100644 mailman/docs/chains.txt delete mode 100644 mailman/docs/domains.txt delete mode 100644 mailman/docs/languages.txt delete mode 100644 mailman/docs/lifecycle.txt delete mode 100644 mailman/docs/listmanager.txt delete mode 100644 mailman/docs/membership.txt delete mode 100644 mailman/docs/message.txt delete mode 100644 mailman/docs/messagestore.txt delete mode 100644 mailman/docs/mlist-addresses.txt delete mode 100644 mailman/docs/pending.txt delete mode 100644 mailman/docs/pipelines.txt delete mode 100644 mailman/docs/registration.txt delete mode 100644 mailman/docs/requests.txt delete mode 100644 mailman/docs/styles.txt delete mode 100644 mailman/docs/usermanager.txt delete mode 100644 mailman/docs/users.txt delete mode 100644 mailman/domain.py delete mode 100644 mailman/extras/__init__.py delete mode 100644 mailman/extras/mailman.cfg.in delete mode 100644 mailman/i18n.py delete mode 100644 mailman/inject.py delete mode 100644 mailman/interact.py delete mode 100644 mailman/interfaces/__init__.py delete mode 100644 mailman/interfaces/address.py delete mode 100644 mailman/interfaces/archiver.py delete mode 100644 mailman/interfaces/chain.py delete mode 100644 mailman/interfaces/command.py delete mode 100644 mailman/interfaces/database.py delete mode 100644 mailman/interfaces/domain.py delete mode 100644 mailman/interfaces/errors.py delete mode 100644 mailman/interfaces/handler.py delete mode 100644 mailman/interfaces/languages.py delete mode 100644 mailman/interfaces/listmanager.py delete mode 100644 mailman/interfaces/mailinglist.py delete mode 100644 mailman/interfaces/member.py delete mode 100644 mailman/interfaces/messages.py delete mode 100644 mailman/interfaces/mlistrequest.py delete mode 100644 mailman/interfaces/mta.py delete mode 100644 mailman/interfaces/pending.py delete mode 100644 mailman/interfaces/permissions.py delete mode 100644 mailman/interfaces/pipeline.py delete mode 100644 mailman/interfaces/preferences.py delete mode 100644 mailman/interfaces/registrar.py delete mode 100644 mailman/interfaces/requests.py delete mode 100644 mailman/interfaces/roster.py delete mode 100644 mailman/interfaces/rules.py delete mode 100644 mailman/interfaces/runner.py delete mode 100644 mailman/interfaces/styles.py delete mode 100644 mailman/interfaces/switchboard.py delete mode 100644 mailman/interfaces/user.py delete mode 100644 mailman/interfaces/usermanager.py delete mode 100644 mailman/languages.py delete mode 100644 mailman/messages/__init__.py delete mode 100644 mailman/messages/ar/LC_MESSAGES/mailman.po delete mode 100644 mailman/messages/ca/LC_MESSAGES/mailman.po delete mode 100644 mailman/messages/cs/LC_MESSAGES/mailman.po delete mode 100644 mailman/messages/da/LC_MESSAGES/mailman.po delete mode 100644 mailman/messages/de/LC_MESSAGES/mailman.po delete mode 100644 mailman/messages/de/README.de delete mode 100644 mailman/messages/docstring.files delete mode 100644 mailman/messages/es/LC_MESSAGES/mailman.po delete mode 100644 mailman/messages/es/README.es delete mode 100644 mailman/messages/et/LC_MESSAGES/mailman.po delete mode 100644 mailman/messages/eu/LC_MESSAGES/mailman.po delete mode 100644 mailman/messages/eu/README.eu delete mode 100644 mailman/messages/fi/LC_MESSAGES/mailman.po delete mode 100644 mailman/messages/fi/README.fi delete mode 100644 mailman/messages/fr/LC_MESSAGES/mailman.po delete mode 100644 mailman/messages/fr/README.fr delete mode 100644 mailman/messages/hr/LC_MESSAGES/mailman.po delete mode 100644 mailman/messages/hu/FAQ.hu delete mode 100644 mailman/messages/hu/INSTALL.hu delete mode 100644 mailman/messages/hu/LC_MESSAGES/mailman.po delete mode 100644 mailman/messages/hu/README.BSD.hu delete mode 100644 mailman/messages/hu/README.CONTRIB.hu delete mode 100644 mailman/messages/hu/README.EXIM.hu delete mode 100644 mailman/messages/hu/README.LINUX.hu delete mode 100644 mailman/messages/hu/README.MACOSX.hu delete mode 100644 mailman/messages/hu/README.NETSCAPE.hu delete mode 100644 mailman/messages/hu/README.POSTFIX.hu delete mode 100644 mailman/messages/hu/README.QMAIL.hu delete mode 100644 mailman/messages/hu/README.SENDMAIL.hu delete mode 100644 mailman/messages/hu/README.USERAGENT.hu delete mode 100644 mailman/messages/hu/README.hu delete mode 100644 mailman/messages/hu/UPGRADING.hu delete mode 100644 mailman/messages/ia/LC_MESSAGES/mailman.po delete mode 100644 mailman/messages/it/LC_MESSAGES/mailman.po delete mode 100644 mailman/messages/it/README.it delete mode 100644 mailman/messages/ja/INSTALL delete mode 100644 mailman/messages/ja/LC_MESSAGES/mailman.po delete mode 100644 mailman/messages/ja/README delete mode 100644 mailman/messages/ja/README.ja delete mode 100644 mailman/messages/ja/UPGRADING delete mode 100644 mailman/messages/ja/doc/Defaults.py.in delete mode 100644 mailman/messages/ja/doc/mailman-install.tex delete mode 100644 mailman/messages/ja/doc/mailman-member.tex delete mode 100644 mailman/messages/ko/LC_MESSAGES/mailman.po delete mode 100644 mailman/messages/ko/README.ko delete mode 100644 mailman/messages/lt/LC_MESSAGES/mailman.po delete mode 100644 mailman/messages/mailman.pot delete mode 100644 mailman/messages/marked.files delete mode 100644 mailman/messages/nl/LC_MESSAGES/mailman.po delete mode 100644 mailman/messages/no/LC_MESSAGES/mailman.po delete mode 100644 mailman/messages/pl/LC_MESSAGES/mailman.po delete mode 100644 mailman/messages/pl/README.pl delete mode 100644 mailman/messages/pt/LC_MESSAGES/mailman.po delete mode 100644 mailman/messages/pt_BR/LC_MESSAGES/mailman.po delete mode 100644 mailman/messages/ro/LC_MESSAGES/mailman.po delete mode 100644 mailman/messages/ru/LC_MESSAGES/mailman.po delete mode 100644 mailman/messages/ru/README.ru delete mode 100644 mailman/messages/sl/LC_MESSAGES/mailman.po delete mode 100644 mailman/messages/sr/LC_MESSAGES/mailman.po delete mode 100644 mailman/messages/sr/readme.sr delete mode 100644 mailman/messages/sv/LC_MESSAGES/mailman.po delete mode 100644 mailman/messages/sv/README.sv delete mode 100644 mailman/messages/tr/LC_MESSAGES/mailman.po delete mode 100644 mailman/messages/uk/LC_MESSAGES/mailman.po delete mode 100644 mailman/messages/vi/LC_MESSAGES/mailman.po delete mode 100644 mailman/messages/zh_CN/LC_MESSAGES/mailman.po delete mode 100644 mailman/messages/zh_TW/LC_MESSAGES/mailman.po delete mode 100644 mailman/mta/__init__.py delete mode 100644 mailman/mta/null.py delete mode 100644 mailman/mta/postfix.py delete mode 100644 mailman/mta/smtp_direct.py delete mode 100644 mailman/options.py delete mode 100644 mailman/passwords.py delete mode 100644 mailman/pipeline/__init__.py delete mode 100644 mailman/pipeline/acknowledge.py delete mode 100644 mailman/pipeline/after_delivery.py delete mode 100644 mailman/pipeline/avoid_duplicates.py delete mode 100644 mailman/pipeline/calculate_recipients.py delete mode 100644 mailman/pipeline/cleanse.py delete mode 100644 mailman/pipeline/cleanse_dkim.py delete mode 100644 mailman/pipeline/cook_headers.py delete mode 100644 mailman/pipeline/decorate.py delete mode 100644 mailman/pipeline/docs/ack-headers.txt delete mode 100644 mailman/pipeline/docs/acknowledge.txt delete mode 100644 mailman/pipeline/docs/after-delivery.txt delete mode 100644 mailman/pipeline/docs/archives.txt delete mode 100644 mailman/pipeline/docs/avoid-duplicates.txt delete mode 100644 mailman/pipeline/docs/calc-recips.txt delete mode 100644 mailman/pipeline/docs/cleanse.txt delete mode 100644 mailman/pipeline/docs/cook-headers.txt delete mode 100644 mailman/pipeline/docs/decorate.txt delete mode 100644 mailman/pipeline/docs/digests.txt delete mode 100644 mailman/pipeline/docs/file-recips.txt delete mode 100644 mailman/pipeline/docs/filtering.txt delete mode 100644 mailman/pipeline/docs/nntp.txt delete mode 100644 mailman/pipeline/docs/reply-to.txt delete mode 100644 mailman/pipeline/docs/replybot.txt delete mode 100644 mailman/pipeline/docs/scrubber.txt delete mode 100644 mailman/pipeline/docs/subject-munging.txt delete mode 100644 mailman/pipeline/docs/tagger.txt delete mode 100644 mailman/pipeline/docs/to-outgoing.txt delete mode 100644 mailman/pipeline/file_recipients.py delete mode 100644 mailman/pipeline/mime_delete.py delete mode 100644 mailman/pipeline/moderate.py delete mode 100644 mailman/pipeline/owner_recipients.py delete mode 100644 mailman/pipeline/replybot.py delete mode 100644 mailman/pipeline/scrubber.py delete mode 100644 mailman/pipeline/tagger.py delete mode 100644 mailman/pipeline/to_archive.py delete mode 100644 mailman/pipeline/to_digest.py delete mode 100644 mailman/pipeline/to_outgoing.py delete mode 100644 mailman/pipeline/to_usenet.py delete mode 100644 mailman/queue/__init__.py delete mode 100644 mailman/queue/archive.py delete mode 100644 mailman/queue/bounce.py delete mode 100644 mailman/queue/command.py delete mode 100644 mailman/queue/docs/OVERVIEW.txt delete mode 100644 mailman/queue/docs/archiver.txt delete mode 100644 mailman/queue/docs/command.txt delete mode 100644 mailman/queue/docs/incoming.txt delete mode 100644 mailman/queue/docs/lmtp.txt delete mode 100644 mailman/queue/docs/news.txt delete mode 100644 mailman/queue/docs/outgoing.txt delete mode 100644 mailman/queue/docs/runner.txt delete mode 100644 mailman/queue/docs/switchboard.txt delete mode 100644 mailman/queue/http.py delete mode 100644 mailman/queue/incoming.py delete mode 100644 mailman/queue/lmtp.py delete mode 100644 mailman/queue/maildir.py delete mode 100644 mailman/queue/news.py delete mode 100644 mailman/queue/outgoing.py delete mode 100644 mailman/queue/pipeline.py delete mode 100644 mailman/queue/retry.py delete mode 100644 mailman/queue/virgin.py delete mode 100644 mailman/rules/__init__.py delete mode 100644 mailman/rules/administrivia.py delete mode 100644 mailman/rules/any.py delete mode 100644 mailman/rules/approved.py delete mode 100644 mailman/rules/docs/administrivia.txt delete mode 100644 mailman/rules/docs/approve.txt delete mode 100644 mailman/rules/docs/emergency.txt delete mode 100644 mailman/rules/docs/header-matching.txt delete mode 100644 mailman/rules/docs/implicit-dest.txt delete mode 100644 mailman/rules/docs/loop.txt delete mode 100644 mailman/rules/docs/max-size.txt delete mode 100644 mailman/rules/docs/moderation.txt delete mode 100644 mailman/rules/docs/news-moderation.txt delete mode 100644 mailman/rules/docs/no-subject.txt delete mode 100644 mailman/rules/docs/recipients.txt delete mode 100644 mailman/rules/docs/rules.txt delete mode 100644 mailman/rules/docs/suspicious.txt delete mode 100644 mailman/rules/docs/truth.txt delete mode 100644 mailman/rules/emergency.py delete mode 100644 mailman/rules/implicit_dest.py delete mode 100644 mailman/rules/loop.py delete mode 100644 mailman/rules/max_recipients.py delete mode 100644 mailman/rules/max_size.py delete mode 100644 mailman/rules/moderation.py delete mode 100644 mailman/rules/news_moderation.py delete mode 100644 mailman/rules/no_subject.py delete mode 100644 mailman/rules/suspicious.py delete mode 100644 mailman/rules/truth.py delete mode 100644 mailman/styles/__init__.py delete mode 100644 mailman/styles/default.py delete mode 100644 mailman/styles/manager.py delete mode 100644 mailman/templates/__init__.py delete mode 100644 mailman/templates/en/__init__.py delete mode 100644 mailman/templates/en/adminaddrchgack.txt delete mode 100644 mailman/templates/en/admindbdetails.html delete mode 100644 mailman/templates/en/admindbpreamble.html delete mode 100644 mailman/templates/en/admindbsummary.html delete mode 100644 mailman/templates/en/adminsubscribeack.txt delete mode 100644 mailman/templates/en/adminunsubscribeack.txt delete mode 100644 mailman/templates/en/admlogin.html delete mode 100644 mailman/templates/en/approve.txt delete mode 100644 mailman/templates/en/archidxentry.html delete mode 100644 mailman/templates/en/archidxfoot.html delete mode 100644 mailman/templates/en/archidxhead.html delete mode 100644 mailman/templates/en/archlistend.html delete mode 100644 mailman/templates/en/archliststart.html delete mode 100644 mailman/templates/en/archtoc.html delete mode 100644 mailman/templates/en/archtocentry.html delete mode 100644 mailman/templates/en/archtocnombox.html delete mode 100644 mailman/templates/en/article.html delete mode 100644 mailman/templates/en/bounce.txt delete mode 100644 mailman/templates/en/checkdbs.txt delete mode 100644 mailman/templates/en/convert.txt delete mode 100644 mailman/templates/en/cronpass.txt delete mode 100644 mailman/templates/en/disabled.txt delete mode 100644 mailman/templates/en/emptyarchive.html delete mode 100644 mailman/templates/en/headfoot.html delete mode 100644 mailman/templates/en/help.txt delete mode 100644 mailman/templates/en/invite.txt delete mode 100644 mailman/templates/en/listinfo.html delete mode 100644 mailman/templates/en/masthead.txt delete mode 100644 mailman/templates/en/newlist.txt delete mode 100644 mailman/templates/en/nomoretoday.txt delete mode 100644 mailman/templates/en/options.html delete mode 100644 mailman/templates/en/postack.txt delete mode 100644 mailman/templates/en/postauth.txt delete mode 100644 mailman/templates/en/postheld.txt delete mode 100644 mailman/templates/en/private.html delete mode 100644 mailman/templates/en/probe.txt delete mode 100644 mailman/templates/en/refuse.txt delete mode 100644 mailman/templates/en/roster.html delete mode 100644 mailman/templates/en/subauth.txt delete mode 100644 mailman/templates/en/subscribe.html delete mode 100644 mailman/templates/en/subscribeack.txt delete mode 100644 mailman/templates/en/unsub.txt delete mode 100644 mailman/templates/en/unsubauth.txt delete mode 100644 mailman/templates/en/userpass.txt delete mode 100644 mailman/templates/en/verify.txt delete mode 100644 mailman/testing/__init__.py delete mode 100644 mailman/testing/helpers.py delete mode 100644 mailman/testing/layers.py delete mode 100644 mailman/testing/mta.py delete mode 100644 mailman/testing/smtplistener.py delete mode 100644 mailman/testing/testing.cfg delete mode 100644 mailman/tests/__init__.py delete mode 100644 mailman/tests/bounces/__init__.py delete mode 100644 mailman/tests/bounces/bounce_01.txt delete mode 100644 mailman/tests/bounces/bounce_02.txt delete mode 100644 mailman/tests/bounces/bounce_03.txt delete mode 100644 mailman/tests/bounces/dsn_01.txt delete mode 100644 mailman/tests/bounces/dsn_02.txt delete mode 100644 mailman/tests/bounces/dsn_03.txt delete mode 100644 mailman/tests/bounces/dsn_04.txt delete mode 100644 mailman/tests/bounces/dsn_05.txt delete mode 100644 mailman/tests/bounces/dsn_06.txt delete mode 100644 mailman/tests/bounces/dsn_07.txt delete mode 100644 mailman/tests/bounces/dsn_08.txt delete mode 100644 mailman/tests/bounces/dsn_09.txt delete mode 100644 mailman/tests/bounces/dsn_10.txt delete mode 100644 mailman/tests/bounces/dsn_11.txt delete mode 100644 mailman/tests/bounces/dsn_12.txt delete mode 100644 mailman/tests/bounces/dsn_13.txt delete mode 100644 mailman/tests/bounces/dsn_14.txt delete mode 100644 mailman/tests/bounces/dsn_15.txt delete mode 100644 mailman/tests/bounces/dumbass_01.txt delete mode 100644 mailman/tests/bounces/exim_01.txt delete mode 100644 mailman/tests/bounces/groupwise_01.txt delete mode 100644 mailman/tests/bounces/groupwise_02.txt delete mode 100644 mailman/tests/bounces/hotpop_01.txt delete mode 100644 mailman/tests/bounces/llnl_01.txt delete mode 100644 mailman/tests/bounces/microsoft_01.txt delete mode 100644 mailman/tests/bounces/microsoft_02.txt delete mode 100644 mailman/tests/bounces/microsoft_03.txt delete mode 100644 mailman/tests/bounces/netscape_01.txt delete mode 100644 mailman/tests/bounces/newmailru_01.txt delete mode 100644 mailman/tests/bounces/postfix_01.txt delete mode 100644 mailman/tests/bounces/postfix_02.txt delete mode 100644 mailman/tests/bounces/postfix_03.txt delete mode 100644 mailman/tests/bounces/postfix_04.txt delete mode 100644 mailman/tests/bounces/postfix_05.txt delete mode 100644 mailman/tests/bounces/qmail_01.txt delete mode 100644 mailman/tests/bounces/qmail_02.txt delete mode 100644 mailman/tests/bounces/qmail_03.txt delete mode 100644 mailman/tests/bounces/qmail_04.txt delete mode 100644 mailman/tests/bounces/qmail_05.txt delete mode 100644 mailman/tests/bounces/sendmail_01.txt delete mode 100644 mailman/tests/bounces/simple_01.txt delete mode 100644 mailman/tests/bounces/simple_02.txt delete mode 100644 mailman/tests/bounces/simple_03.txt delete mode 100644 mailman/tests/bounces/simple_04.txt delete mode 100644 mailman/tests/bounces/simple_05.txt delete mode 100644 mailman/tests/bounces/simple_06.txt delete mode 100644 mailman/tests/bounces/simple_07.txt delete mode 100644 mailman/tests/bounces/simple_08.txt delete mode 100644 mailman/tests/bounces/simple_09.txt delete mode 100644 mailman/tests/bounces/simple_10.txt delete mode 100644 mailman/tests/bounces/simple_11.txt delete mode 100644 mailman/tests/bounces/simple_12.txt delete mode 100644 mailman/tests/bounces/simple_13.txt delete mode 100644 mailman/tests/bounces/simple_14.txt delete mode 100644 mailman/tests/bounces/simple_15.txt delete mode 100644 mailman/tests/bounces/simple_16.txt delete mode 100644 mailman/tests/bounces/simple_17.txt delete mode 100644 mailman/tests/bounces/simple_18.txt delete mode 100644 mailman/tests/bounces/simple_19.txt delete mode 100644 mailman/tests/bounces/simple_20.txt delete mode 100644 mailman/tests/bounces/simple_21.txt delete mode 100644 mailman/tests/bounces/simple_22.txt delete mode 100644 mailman/tests/bounces/simple_23.txt delete mode 100644 mailman/tests/bounces/simple_24.txt delete mode 100644 mailman/tests/bounces/simple_25.txt delete mode 100644 mailman/tests/bounces/simple_26.txt delete mode 100644 mailman/tests/bounces/simple_27.txt delete mode 100644 mailman/tests/bounces/sina_01.txt delete mode 100644 mailman/tests/bounces/smtp32_01.txt delete mode 100644 mailman/tests/bounces/smtp32_02.txt delete mode 100644 mailman/tests/bounces/smtp32_03.txt delete mode 100644 mailman/tests/bounces/smtp32_04.txt delete mode 100644 mailman/tests/bounces/smtp32_05.txt delete mode 100644 mailman/tests/bounces/smtp32_06.txt delete mode 100755 mailman/tests/bounces/smtp32_07.txt delete mode 100644 mailman/tests/bounces/yahoo_01.txt delete mode 100644 mailman/tests/bounces/yahoo_02.txt delete mode 100644 mailman/tests/bounces/yahoo_03.txt delete mode 100644 mailman/tests/bounces/yahoo_04.txt delete mode 100644 mailman/tests/bounces/yahoo_05.txt delete mode 100644 mailman/tests/bounces/yahoo_06.txt delete mode 100644 mailman/tests/bounces/yahoo_07.txt delete mode 100644 mailman/tests/bounces/yahoo_08.txt delete mode 100644 mailman/tests/bounces/yahoo_09.txt delete mode 100644 mailman/tests/bounces/yahoo_10.txt delete mode 100644 mailman/tests/bounces/yale_01.txt delete mode 100644 mailman/tests/test_bounces.py delete mode 100644 mailman/tests/test_documentation.py delete mode 100644 mailman/tests/test_membership.py delete mode 100644 mailman/tests/test_passwords.py delete mode 100644 mailman/tests/test_security_mgr.py delete mode 100644 mailman/utilities/__init__.py delete mode 100644 mailman/utilities/filesystem.py delete mode 100644 mailman/utilities/string.py delete mode 100644 mailman/version.py delete mode 100644 mailman/web/Cgi/Auth.py delete mode 100644 mailman/web/Cgi/__init__.py delete mode 100644 mailman/web/Cgi/admin.py delete mode 100644 mailman/web/Cgi/admindb.py delete mode 100644 mailman/web/Cgi/confirm.py delete mode 100644 mailman/web/Cgi/create.py delete mode 100644 mailman/web/Cgi/edithtml.py delete mode 100644 mailman/web/Cgi/listinfo.py delete mode 100644 mailman/web/Cgi/options.py delete mode 100644 mailman/web/Cgi/private.py delete mode 100644 mailman/web/Cgi/rmlist.py delete mode 100644 mailman/web/Cgi/roster.py delete mode 100644 mailman/web/Cgi/subscribe.py delete mode 100644 mailman/web/Cgi/wsgi_app.py delete mode 100644 mailman/web/Gui/Archive.py delete mode 100644 mailman/web/Gui/Autoresponse.py delete mode 100644 mailman/web/Gui/Bounce.py delete mode 100644 mailman/web/Gui/ContentFilter.py delete mode 100644 mailman/web/Gui/Digest.py delete mode 100644 mailman/web/Gui/GUIBase.py delete mode 100644 mailman/web/Gui/General.py delete mode 100644 mailman/web/Gui/Language.py delete mode 100644 mailman/web/Gui/Membership.py delete mode 100644 mailman/web/Gui/NonDigest.py delete mode 100644 mailman/web/Gui/Passwords.py delete mode 100644 mailman/web/Gui/Privacy.py delete mode 100644 mailman/web/Gui/Topics.py delete mode 100644 mailman/web/Gui/Usenet.py delete mode 100644 mailman/web/Gui/__init__.py delete mode 100644 mailman/web/HTMLFormatter.py delete mode 100644 mailman/web/__init__.py delete mode 100644 mailman/web/htmlformat.py create mode 100644 src/mailman/Archiver/Archiver.py create mode 100644 src/mailman/Archiver/HyperArch.py create mode 100644 src/mailman/Archiver/HyperDatabase.py create mode 100644 src/mailman/Archiver/__init__.py create mode 100644 src/mailman/Archiver/pipermail.py create mode 100644 src/mailman/Bouncers/BouncerAPI.py create mode 100644 src/mailman/Bouncers/Caiwireless.py create mode 100644 src/mailman/Bouncers/Compuserve.py create mode 100644 src/mailman/Bouncers/DSN.py create mode 100644 src/mailman/Bouncers/Exchange.py create mode 100644 src/mailman/Bouncers/Exim.py create mode 100644 src/mailman/Bouncers/GroupWise.py create mode 100644 src/mailman/Bouncers/LLNL.py create mode 100644 src/mailman/Bouncers/Microsoft.py create mode 100644 src/mailman/Bouncers/Netscape.py create mode 100644 src/mailman/Bouncers/Postfix.py create mode 100644 src/mailman/Bouncers/Qmail.py create mode 100644 src/mailman/Bouncers/SMTP32.py create mode 100644 src/mailman/Bouncers/SimpleMatch.py create mode 100644 src/mailman/Bouncers/SimpleWarning.py create mode 100644 src/mailman/Bouncers/Sina.py create mode 100644 src/mailman/Bouncers/Yahoo.py create mode 100644 src/mailman/Bouncers/Yale.py create mode 100644 src/mailman/Bouncers/__init__.py create mode 100644 src/mailman/Mailbox.py create mode 100644 src/mailman/Message.py create mode 100644 src/mailman/Utils.py create mode 100644 src/mailman/__init__.py create mode 100644 src/mailman/app/__init__.py create mode 100644 src/mailman/app/bounces.py create mode 100644 src/mailman/app/commands.py create mode 100644 src/mailman/app/lifecycle.py create mode 100644 src/mailman/app/membership.py create mode 100644 src/mailman/app/moderator.py create mode 100644 src/mailman/app/notifications.py create mode 100644 src/mailman/app/registrar.py create mode 100644 src/mailman/app/replybot.py create mode 100644 src/mailman/archiving/__init__.py create mode 100644 src/mailman/archiving/mailarchive.py create mode 100644 src/mailman/archiving/mhonarc.py create mode 100644 src/mailman/archiving/pipermail.py create mode 100644 src/mailman/archiving/prototype.py create mode 100644 src/mailman/attic/Bouncer.py create mode 100644 src/mailman/attic/Defaults.py create mode 100644 src/mailman/attic/Deliverer.py create mode 100644 src/mailman/attic/Digester.py create mode 100644 src/mailman/attic/MailList.py create mode 100644 src/mailman/attic/SecurityManager.py create mode 100755 src/mailman/attic/bin/clone_member create mode 100644 src/mailman/attic/bin/discard create mode 100644 src/mailman/attic/bin/fix_url.py create mode 100644 src/mailman/attic/bin/list_admins create mode 100644 src/mailman/attic/bin/msgfmt.py create mode 100644 src/mailman/attic/bin/po2templ.py create mode 100644 src/mailman/attic/bin/pygettext.py create mode 100755 src/mailman/attic/bin/remove_members create mode 100644 src/mailman/attic/bin/reset_pw.py create mode 100755 src/mailman/attic/bin/sync_members create mode 100644 src/mailman/attic/bin/templ2pot.py create mode 100755 src/mailman/attic/bin/transcheck create mode 100644 src/mailman/bin/__init__.py create mode 100644 src/mailman/bin/add_members.py create mode 100644 src/mailman/bin/arch.py create mode 100644 src/mailman/bin/bumpdigests.py create mode 100644 src/mailman/bin/check_perms.py create mode 100644 src/mailman/bin/checkdbs.py create mode 100644 src/mailman/bin/cleanarch.py create mode 100644 src/mailman/bin/config_list.py create mode 100644 src/mailman/bin/create_list.py create mode 100644 src/mailman/bin/disabled.py create mode 100644 src/mailman/bin/docs/master.txt create mode 100644 src/mailman/bin/dumpdb.py create mode 100644 src/mailman/bin/export.py create mode 100644 src/mailman/bin/find_member.py create mode 100644 src/mailman/bin/gate_news.py create mode 100644 src/mailman/bin/genaliases.py create mode 100644 src/mailman/bin/import.py create mode 100644 src/mailman/bin/inject.py create mode 100644 src/mailman/bin/list_lists.py create mode 100644 src/mailman/bin/list_members.py create mode 100644 src/mailman/bin/list_owners.py create mode 100644 src/mailman/bin/mailmanctl.py create mode 100644 src/mailman/bin/master.py create mode 100644 src/mailman/bin/mmsitepass.py create mode 100644 src/mailman/bin/nightly_gzip.py create mode 100644 src/mailman/bin/qrunner.py create mode 100644 src/mailman/bin/remove_list.py create mode 100644 src/mailman/bin/senddigests.py create mode 100644 src/mailman/bin/set_members.py create mode 100644 src/mailman/bin/show_config.py create mode 100644 src/mailman/bin/show_qfiles.py create mode 100644 src/mailman/bin/unshunt.py create mode 100644 src/mailman/bin/update.py create mode 100644 src/mailman/bin/version.py create mode 100644 src/mailman/bin/withlist.py create mode 100644 src/mailman/chains/__init__.py create mode 100644 src/mailman/chains/accept.py create mode 100644 src/mailman/chains/base.py create mode 100644 src/mailman/chains/builtin.py create mode 100644 src/mailman/chains/discard.py create mode 100644 src/mailman/chains/headers.py create mode 100644 src/mailman/chains/hold.py create mode 100644 src/mailman/chains/reject.py create mode 100644 src/mailman/commands/__init__.py create mode 100644 src/mailman/commands/cmd_confirm.py create mode 100644 src/mailman/commands/cmd_help.py create mode 100644 src/mailman/commands/cmd_info.py create mode 100644 src/mailman/commands/cmd_leave.py create mode 100644 src/mailman/commands/cmd_lists.py create mode 100644 src/mailman/commands/cmd_password.py create mode 100644 src/mailman/commands/cmd_remove.py create mode 100644 src/mailman/commands/cmd_set.py create mode 100644 src/mailman/commands/cmd_unsubscribe.py create mode 100644 src/mailman/commands/cmd_who.py create mode 100644 src/mailman/commands/docs/echo.txt create mode 100644 src/mailman/commands/docs/end.txt create mode 100644 src/mailman/commands/docs/join.txt create mode 100644 src/mailman/commands/echo.py create mode 100644 src/mailman/commands/end.py create mode 100644 src/mailman/commands/join.py create mode 100644 src/mailman/config/__init__.py create mode 100644 src/mailman/config/config.py create mode 100644 src/mailman/config/mailman.cfg create mode 100644 src/mailman/config/schema.cfg create mode 100644 src/mailman/constants.py create mode 100644 src/mailman/core/__init__.py create mode 100644 src/mailman/core/chains.py create mode 100644 src/mailman/core/errors.py create mode 100644 src/mailman/core/initialize.py create mode 100644 src/mailman/core/logging.py create mode 100644 src/mailman/core/pipelines.py create mode 100644 src/mailman/core/plugins.py create mode 100644 src/mailman/core/rules.py create mode 100644 src/mailman/database/__init__.py create mode 100644 src/mailman/database/address.py create mode 100644 src/mailman/database/language.py create mode 100644 src/mailman/database/listmanager.py create mode 100644 src/mailman/database/mailinglist.py create mode 100644 src/mailman/database/mailman.sql create mode 100644 src/mailman/database/member.py create mode 100644 src/mailman/database/message.py create mode 100644 src/mailman/database/messagestore.py create mode 100644 src/mailman/database/model.py create mode 100644 src/mailman/database/pending.py create mode 100644 src/mailman/database/preferences.py create mode 100644 src/mailman/database/requests.py create mode 100644 src/mailman/database/roster.py create mode 100644 src/mailman/database/transaction.py create mode 100644 src/mailman/database/types.py create mode 100644 src/mailman/database/user.py create mode 100644 src/mailman/database/usermanager.py create mode 100644 src/mailman/database/version.py create mode 100644 src/mailman/docs/__init__.py create mode 100644 src/mailman/docs/addresses.txt create mode 100644 src/mailman/docs/archivers.txt create mode 100644 src/mailman/docs/bounces.txt create mode 100644 src/mailman/docs/chains.txt create mode 100644 src/mailman/docs/domains.txt create mode 100644 src/mailman/docs/languages.txt create mode 100644 src/mailman/docs/lifecycle.txt create mode 100644 src/mailman/docs/listmanager.txt create mode 100644 src/mailman/docs/membership.txt create mode 100644 src/mailman/docs/message.txt create mode 100644 src/mailman/docs/messagestore.txt create mode 100644 src/mailman/docs/mlist-addresses.txt create mode 100644 src/mailman/docs/pending.txt create mode 100644 src/mailman/docs/pipelines.txt create mode 100644 src/mailman/docs/registration.txt create mode 100644 src/mailman/docs/requests.txt create mode 100644 src/mailman/docs/styles.txt create mode 100644 src/mailman/docs/usermanager.txt create mode 100644 src/mailman/docs/users.txt create mode 100644 src/mailman/domain.py create mode 100644 src/mailman/extras/__init__.py create mode 100644 src/mailman/extras/mailman.cfg.in create mode 100644 src/mailman/i18n.py create mode 100644 src/mailman/inject.py create mode 100644 src/mailman/interact.py create mode 100644 src/mailman/interfaces/__init__.py create mode 100644 src/mailman/interfaces/address.py create mode 100644 src/mailman/interfaces/archiver.py create mode 100644 src/mailman/interfaces/chain.py create mode 100644 src/mailman/interfaces/command.py create mode 100644 src/mailman/interfaces/database.py create mode 100644 src/mailman/interfaces/domain.py create mode 100644 src/mailman/interfaces/errors.py create mode 100644 src/mailman/interfaces/handler.py create mode 100644 src/mailman/interfaces/languages.py create mode 100644 src/mailman/interfaces/listmanager.py create mode 100644 src/mailman/interfaces/mailinglist.py create mode 100644 src/mailman/interfaces/member.py create mode 100644 src/mailman/interfaces/messages.py create mode 100644 src/mailman/interfaces/mlistrequest.py create mode 100644 src/mailman/interfaces/mta.py create mode 100644 src/mailman/interfaces/pending.py create mode 100644 src/mailman/interfaces/permissions.py create mode 100644 src/mailman/interfaces/pipeline.py create mode 100644 src/mailman/interfaces/preferences.py create mode 100644 src/mailman/interfaces/registrar.py create mode 100644 src/mailman/interfaces/requests.py create mode 100644 src/mailman/interfaces/roster.py create mode 100644 src/mailman/interfaces/rules.py create mode 100644 src/mailman/interfaces/runner.py create mode 100644 src/mailman/interfaces/styles.py create mode 100644 src/mailman/interfaces/switchboard.py create mode 100644 src/mailman/interfaces/user.py create mode 100644 src/mailman/interfaces/usermanager.py create mode 100644 src/mailman/languages.py create mode 100644 src/mailman/messages/__init__.py create mode 100644 src/mailman/messages/ar/LC_MESSAGES/mailman.po create mode 100644 src/mailman/messages/ca/LC_MESSAGES/mailman.po create mode 100644 src/mailman/messages/cs/LC_MESSAGES/mailman.po create mode 100644 src/mailman/messages/da/LC_MESSAGES/mailman.po create mode 100644 src/mailman/messages/de/LC_MESSAGES/mailman.po create mode 100644 src/mailman/messages/de/README.de create mode 100644 src/mailman/messages/docstring.files create mode 100644 src/mailman/messages/es/LC_MESSAGES/mailman.po create mode 100644 src/mailman/messages/es/README.es create mode 100644 src/mailman/messages/et/LC_MESSAGES/mailman.po create mode 100644 src/mailman/messages/eu/LC_MESSAGES/mailman.po create mode 100644 src/mailman/messages/eu/README.eu create mode 100644 src/mailman/messages/fi/LC_MESSAGES/mailman.po create mode 100644 src/mailman/messages/fi/README.fi create mode 100644 src/mailman/messages/fr/LC_MESSAGES/mailman.po create mode 100644 src/mailman/messages/fr/README.fr create mode 100644 src/mailman/messages/hr/LC_MESSAGES/mailman.po create mode 100644 src/mailman/messages/hu/FAQ.hu create mode 100644 src/mailman/messages/hu/INSTALL.hu create mode 100644 src/mailman/messages/hu/LC_MESSAGES/mailman.po create mode 100644 src/mailman/messages/hu/README.BSD.hu create mode 100644 src/mailman/messages/hu/README.CONTRIB.hu create mode 100644 src/mailman/messages/hu/README.EXIM.hu create mode 100644 src/mailman/messages/hu/README.LINUX.hu create mode 100644 src/mailman/messages/hu/README.MACOSX.hu create mode 100644 src/mailman/messages/hu/README.NETSCAPE.hu create mode 100644 src/mailman/messages/hu/README.POSTFIX.hu create mode 100644 src/mailman/messages/hu/README.QMAIL.hu create mode 100644 src/mailman/messages/hu/README.SENDMAIL.hu create mode 100644 src/mailman/messages/hu/README.USERAGENT.hu create mode 100644 src/mailman/messages/hu/README.hu create mode 100644 src/mailman/messages/hu/UPGRADING.hu create mode 100644 src/mailman/messages/ia/LC_MESSAGES/mailman.po create mode 100644 src/mailman/messages/it/LC_MESSAGES/mailman.po create mode 100644 src/mailman/messages/it/README.it create mode 100644 src/mailman/messages/ja/INSTALL create mode 100644 src/mailman/messages/ja/LC_MESSAGES/mailman.po create mode 100644 src/mailman/messages/ja/README create mode 100644 src/mailman/messages/ja/README.ja create mode 100644 src/mailman/messages/ja/UPGRADING create mode 100644 src/mailman/messages/ja/doc/Defaults.py.in create mode 100644 src/mailman/messages/ja/doc/mailman-install.tex create mode 100644 src/mailman/messages/ja/doc/mailman-member.tex create mode 100644 src/mailman/messages/ko/LC_MESSAGES/mailman.po create mode 100644 src/mailman/messages/ko/README.ko create mode 100644 src/mailman/messages/lt/LC_MESSAGES/mailman.po create mode 100644 src/mailman/messages/mailman.pot create mode 100644 src/mailman/messages/marked.files create mode 100644 src/mailman/messages/nl/LC_MESSAGES/mailman.po create mode 100644 src/mailman/messages/no/LC_MESSAGES/mailman.po create mode 100644 src/mailman/messages/pl/LC_MESSAGES/mailman.po create mode 100644 src/mailman/messages/pl/README.pl create mode 100644 src/mailman/messages/pt/LC_MESSAGES/mailman.po create mode 100644 src/mailman/messages/pt_BR/LC_MESSAGES/mailman.po create mode 100644 src/mailman/messages/ro/LC_MESSAGES/mailman.po create mode 100644 src/mailman/messages/ru/LC_MESSAGES/mailman.po create mode 100644 src/mailman/messages/ru/README.ru create mode 100644 src/mailman/messages/sl/LC_MESSAGES/mailman.po create mode 100644 src/mailman/messages/sr/LC_MESSAGES/mailman.po create mode 100644 src/mailman/messages/sr/readme.sr create mode 100644 src/mailman/messages/sv/LC_MESSAGES/mailman.po create mode 100644 src/mailman/messages/sv/README.sv create mode 100644 src/mailman/messages/tr/LC_MESSAGES/mailman.po create mode 100644 src/mailman/messages/uk/LC_MESSAGES/mailman.po create mode 100644 src/mailman/messages/vi/LC_MESSAGES/mailman.po create mode 100644 src/mailman/messages/zh_CN/LC_MESSAGES/mailman.po create mode 100644 src/mailman/messages/zh_TW/LC_MESSAGES/mailman.po create mode 100644 src/mailman/mta/__init__.py create mode 100644 src/mailman/mta/null.py create mode 100644 src/mailman/mta/postfix.py create mode 100644 src/mailman/mta/smtp_direct.py create mode 100644 src/mailman/options.py create mode 100644 src/mailman/passwords.py create mode 100644 src/mailman/pipeline/__init__.py create mode 100644 src/mailman/pipeline/acknowledge.py create mode 100644 src/mailman/pipeline/after_delivery.py create mode 100644 src/mailman/pipeline/avoid_duplicates.py create mode 100644 src/mailman/pipeline/calculate_recipients.py create mode 100644 src/mailman/pipeline/cleanse.py create mode 100644 src/mailman/pipeline/cleanse_dkim.py create mode 100644 src/mailman/pipeline/cook_headers.py create mode 100644 src/mailman/pipeline/decorate.py create mode 100644 src/mailman/pipeline/docs/ack-headers.txt create mode 100644 src/mailman/pipeline/docs/acknowledge.txt create mode 100644 src/mailman/pipeline/docs/after-delivery.txt create mode 100644 src/mailman/pipeline/docs/archives.txt create mode 100644 src/mailman/pipeline/docs/avoid-duplicates.txt create mode 100644 src/mailman/pipeline/docs/calc-recips.txt create mode 100644 src/mailman/pipeline/docs/cleanse.txt create mode 100644 src/mailman/pipeline/docs/cook-headers.txt create mode 100644 src/mailman/pipeline/docs/decorate.txt create mode 100644 src/mailman/pipeline/docs/digests.txt create mode 100644 src/mailman/pipeline/docs/file-recips.txt create mode 100644 src/mailman/pipeline/docs/filtering.txt create mode 100644 src/mailman/pipeline/docs/nntp.txt create mode 100644 src/mailman/pipeline/docs/reply-to.txt create mode 100644 src/mailman/pipeline/docs/replybot.txt create mode 100644 src/mailman/pipeline/docs/scrubber.txt create mode 100644 src/mailman/pipeline/docs/subject-munging.txt create mode 100644 src/mailman/pipeline/docs/tagger.txt create mode 100644 src/mailman/pipeline/docs/to-outgoing.txt create mode 100644 src/mailman/pipeline/file_recipients.py create mode 100644 src/mailman/pipeline/mime_delete.py create mode 100644 src/mailman/pipeline/moderate.py create mode 100644 src/mailman/pipeline/owner_recipients.py create mode 100644 src/mailman/pipeline/replybot.py create mode 100644 src/mailman/pipeline/scrubber.py create mode 100644 src/mailman/pipeline/tagger.py create mode 100644 src/mailman/pipeline/to_archive.py create mode 100644 src/mailman/pipeline/to_digest.py create mode 100644 src/mailman/pipeline/to_outgoing.py create mode 100644 src/mailman/pipeline/to_usenet.py create mode 100644 src/mailman/queue/__init__.py create mode 100644 src/mailman/queue/archive.py create mode 100644 src/mailman/queue/bounce.py create mode 100644 src/mailman/queue/command.py create mode 100644 src/mailman/queue/docs/OVERVIEW.txt create mode 100644 src/mailman/queue/docs/archiver.txt create mode 100644 src/mailman/queue/docs/command.txt create mode 100644 src/mailman/queue/docs/incoming.txt create mode 100644 src/mailman/queue/docs/lmtp.txt create mode 100644 src/mailman/queue/docs/news.txt create mode 100644 src/mailman/queue/docs/outgoing.txt create mode 100644 src/mailman/queue/docs/runner.txt create mode 100644 src/mailman/queue/docs/switchboard.txt create mode 100644 src/mailman/queue/http.py create mode 100644 src/mailman/queue/incoming.py create mode 100644 src/mailman/queue/lmtp.py create mode 100644 src/mailman/queue/maildir.py create mode 100644 src/mailman/queue/news.py create mode 100644 src/mailman/queue/outgoing.py create mode 100644 src/mailman/queue/pipeline.py create mode 100644 src/mailman/queue/retry.py create mode 100644 src/mailman/queue/virgin.py create mode 100644 src/mailman/rules/__init__.py create mode 100644 src/mailman/rules/administrivia.py create mode 100644 src/mailman/rules/any.py create mode 100644 src/mailman/rules/approved.py create mode 100644 src/mailman/rules/docs/administrivia.txt create mode 100644 src/mailman/rules/docs/approve.txt create mode 100644 src/mailman/rules/docs/emergency.txt create mode 100644 src/mailman/rules/docs/header-matching.txt create mode 100644 src/mailman/rules/docs/implicit-dest.txt create mode 100644 src/mailman/rules/docs/loop.txt create mode 100644 src/mailman/rules/docs/max-size.txt create mode 100644 src/mailman/rules/docs/moderation.txt create mode 100644 src/mailman/rules/docs/news-moderation.txt create mode 100644 src/mailman/rules/docs/no-subject.txt create mode 100644 src/mailman/rules/docs/recipients.txt create mode 100644 src/mailman/rules/docs/rules.txt create mode 100644 src/mailman/rules/docs/suspicious.txt create mode 100644 src/mailman/rules/docs/truth.txt create mode 100644 src/mailman/rules/emergency.py create mode 100644 src/mailman/rules/implicit_dest.py create mode 100644 src/mailman/rules/loop.py create mode 100644 src/mailman/rules/max_recipients.py create mode 100644 src/mailman/rules/max_size.py create mode 100644 src/mailman/rules/moderation.py create mode 100644 src/mailman/rules/news_moderation.py create mode 100644 src/mailman/rules/no_subject.py create mode 100644 src/mailman/rules/suspicious.py create mode 100644 src/mailman/rules/truth.py create mode 100644 src/mailman/styles/__init__.py create mode 100644 src/mailman/styles/default.py create mode 100644 src/mailman/styles/manager.py create mode 100644 src/mailman/templates/__init__.py create mode 100644 src/mailman/templates/en/__init__.py create mode 100644 src/mailman/templates/en/adminaddrchgack.txt create mode 100644 src/mailman/templates/en/admindbdetails.html create mode 100644 src/mailman/templates/en/admindbpreamble.html create mode 100644 src/mailman/templates/en/admindbsummary.html create mode 100644 src/mailman/templates/en/adminsubscribeack.txt create mode 100644 src/mailman/templates/en/adminunsubscribeack.txt create mode 100644 src/mailman/templates/en/admlogin.html create mode 100644 src/mailman/templates/en/approve.txt create mode 100644 src/mailman/templates/en/archidxentry.html create mode 100644 src/mailman/templates/en/archidxfoot.html create mode 100644 src/mailman/templates/en/archidxhead.html create mode 100644 src/mailman/templates/en/archlistend.html create mode 100644 src/mailman/templates/en/archliststart.html create mode 100644 src/mailman/templates/en/archtoc.html create mode 100644 src/mailman/templates/en/archtocentry.html create mode 100644 src/mailman/templates/en/archtocnombox.html create mode 100644 src/mailman/templates/en/article.html create mode 100644 src/mailman/templates/en/bounce.txt create mode 100644 src/mailman/templates/en/checkdbs.txt create mode 100644 src/mailman/templates/en/convert.txt create mode 100644 src/mailman/templates/en/cronpass.txt create mode 100644 src/mailman/templates/en/disabled.txt create mode 100644 src/mailman/templates/en/emptyarchive.html create mode 100644 src/mailman/templates/en/headfoot.html create mode 100644 src/mailman/templates/en/help.txt create mode 100644 src/mailman/templates/en/invite.txt create mode 100644 src/mailman/templates/en/listinfo.html create mode 100644 src/mailman/templates/en/masthead.txt create mode 100644 src/mailman/templates/en/newlist.txt create mode 100644 src/mailman/templates/en/nomoretoday.txt create mode 100644 src/mailman/templates/en/options.html create mode 100644 src/mailman/templates/en/postack.txt create mode 100644 src/mailman/templates/en/postauth.txt create mode 100644 src/mailman/templates/en/postheld.txt create mode 100644 src/mailman/templates/en/private.html create mode 100644 src/mailman/templates/en/probe.txt create mode 100644 src/mailman/templates/en/refuse.txt create mode 100644 src/mailman/templates/en/roster.html create mode 100644 src/mailman/templates/en/subauth.txt create mode 100644 src/mailman/templates/en/subscribe.html create mode 100644 src/mailman/templates/en/subscribeack.txt create mode 100644 src/mailman/templates/en/unsub.txt create mode 100644 src/mailman/templates/en/unsubauth.txt create mode 100644 src/mailman/templates/en/userpass.txt create mode 100644 src/mailman/templates/en/verify.txt create mode 100644 src/mailman/testing/__init__.py create mode 100644 src/mailman/testing/helpers.py create mode 100644 src/mailman/testing/layers.py create mode 100644 src/mailman/testing/mta.py create mode 100644 src/mailman/testing/smtplistener.py create mode 100644 src/mailman/testing/testing.cfg create mode 100644 src/mailman/tests/__init__.py create mode 100644 src/mailman/tests/bounces/__init__.py create mode 100644 src/mailman/tests/bounces/bounce_01.txt create mode 100644 src/mailman/tests/bounces/bounce_02.txt create mode 100644 src/mailman/tests/bounces/bounce_03.txt create mode 100644 src/mailman/tests/bounces/dsn_01.txt create mode 100644 src/mailman/tests/bounces/dsn_02.txt create mode 100644 src/mailman/tests/bounces/dsn_03.txt create mode 100644 src/mailman/tests/bounces/dsn_04.txt create mode 100644 src/mailman/tests/bounces/dsn_05.txt create mode 100644 src/mailman/tests/bounces/dsn_06.txt create mode 100644 src/mailman/tests/bounces/dsn_07.txt create mode 100644 src/mailman/tests/bounces/dsn_08.txt create mode 100644 src/mailman/tests/bounces/dsn_09.txt create mode 100644 src/mailman/tests/bounces/dsn_10.txt create mode 100644 src/mailman/tests/bounces/dsn_11.txt create mode 100644 src/mailman/tests/bounces/dsn_12.txt create mode 100644 src/mailman/tests/bounces/dsn_13.txt create mode 100644 src/mailman/tests/bounces/dsn_14.txt create mode 100644 src/mailman/tests/bounces/dsn_15.txt create mode 100644 src/mailman/tests/bounces/dumbass_01.txt create mode 100644 src/mailman/tests/bounces/exim_01.txt create mode 100644 src/mailman/tests/bounces/groupwise_01.txt create mode 100644 src/mailman/tests/bounces/groupwise_02.txt create mode 100644 src/mailman/tests/bounces/hotpop_01.txt create mode 100644 src/mailman/tests/bounces/llnl_01.txt create mode 100644 src/mailman/tests/bounces/microsoft_01.txt create mode 100644 src/mailman/tests/bounces/microsoft_02.txt create mode 100644 src/mailman/tests/bounces/microsoft_03.txt create mode 100644 src/mailman/tests/bounces/netscape_01.txt create mode 100644 src/mailman/tests/bounces/newmailru_01.txt create mode 100644 src/mailman/tests/bounces/postfix_01.txt create mode 100644 src/mailman/tests/bounces/postfix_02.txt create mode 100644 src/mailman/tests/bounces/postfix_03.txt create mode 100644 src/mailman/tests/bounces/postfix_04.txt create mode 100644 src/mailman/tests/bounces/postfix_05.txt create mode 100644 src/mailman/tests/bounces/qmail_01.txt create mode 100644 src/mailman/tests/bounces/qmail_02.txt create mode 100644 src/mailman/tests/bounces/qmail_03.txt create mode 100644 src/mailman/tests/bounces/qmail_04.txt create mode 100644 src/mailman/tests/bounces/qmail_05.txt create mode 100644 src/mailman/tests/bounces/sendmail_01.txt create mode 100644 src/mailman/tests/bounces/simple_01.txt create mode 100644 src/mailman/tests/bounces/simple_02.txt create mode 100644 src/mailman/tests/bounces/simple_03.txt create mode 100644 src/mailman/tests/bounces/simple_04.txt create mode 100644 src/mailman/tests/bounces/simple_05.txt create mode 100644 src/mailman/tests/bounces/simple_06.txt create mode 100644 src/mailman/tests/bounces/simple_07.txt create mode 100644 src/mailman/tests/bounces/simple_08.txt create mode 100644 src/mailman/tests/bounces/simple_09.txt create mode 100644 src/mailman/tests/bounces/simple_10.txt create mode 100644 src/mailman/tests/bounces/simple_11.txt create mode 100644 src/mailman/tests/bounces/simple_12.txt create mode 100644 src/mailman/tests/bounces/simple_13.txt create mode 100644 src/mailman/tests/bounces/simple_14.txt create mode 100644 src/mailman/tests/bounces/simple_15.txt create mode 100644 src/mailman/tests/bounces/simple_16.txt create mode 100644 src/mailman/tests/bounces/simple_17.txt create mode 100644 src/mailman/tests/bounces/simple_18.txt create mode 100644 src/mailman/tests/bounces/simple_19.txt create mode 100644 src/mailman/tests/bounces/simple_20.txt create mode 100644 src/mailman/tests/bounces/simple_21.txt create mode 100644 src/mailman/tests/bounces/simple_22.txt create mode 100644 src/mailman/tests/bounces/simple_23.txt create mode 100644 src/mailman/tests/bounces/simple_24.txt create mode 100644 src/mailman/tests/bounces/simple_25.txt create mode 100644 src/mailman/tests/bounces/simple_26.txt create mode 100644 src/mailman/tests/bounces/simple_27.txt create mode 100644 src/mailman/tests/bounces/sina_01.txt create mode 100644 src/mailman/tests/bounces/smtp32_01.txt create mode 100644 src/mailman/tests/bounces/smtp32_02.txt create mode 100644 src/mailman/tests/bounces/smtp32_03.txt create mode 100644 src/mailman/tests/bounces/smtp32_04.txt create mode 100644 src/mailman/tests/bounces/smtp32_05.txt create mode 100644 src/mailman/tests/bounces/smtp32_06.txt create mode 100755 src/mailman/tests/bounces/smtp32_07.txt create mode 100644 src/mailman/tests/bounces/yahoo_01.txt create mode 100644 src/mailman/tests/bounces/yahoo_02.txt create mode 100644 src/mailman/tests/bounces/yahoo_03.txt create mode 100644 src/mailman/tests/bounces/yahoo_04.txt create mode 100644 src/mailman/tests/bounces/yahoo_05.txt create mode 100644 src/mailman/tests/bounces/yahoo_06.txt create mode 100644 src/mailman/tests/bounces/yahoo_07.txt create mode 100644 src/mailman/tests/bounces/yahoo_08.txt create mode 100644 src/mailman/tests/bounces/yahoo_09.txt create mode 100644 src/mailman/tests/bounces/yahoo_10.txt create mode 100644 src/mailman/tests/bounces/yale_01.txt create mode 100644 src/mailman/tests/test_bounces.py create mode 100644 src/mailman/tests/test_documentation.py create mode 100644 src/mailman/tests/test_membership.py create mode 100644 src/mailman/tests/test_passwords.py create mode 100644 src/mailman/tests/test_security_mgr.py create mode 100644 src/mailman/utilities/__init__.py create mode 100644 src/mailman/utilities/filesystem.py create mode 100644 src/mailman/utilities/string.py create mode 100644 src/mailman/version.py create mode 100644 src/mailman/web/Cgi/Auth.py create mode 100644 src/mailman/web/Cgi/__init__.py create mode 100644 src/mailman/web/Cgi/admin.py create mode 100644 src/mailman/web/Cgi/admindb.py create mode 100644 src/mailman/web/Cgi/confirm.py create mode 100644 src/mailman/web/Cgi/create.py create mode 100644 src/mailman/web/Cgi/edithtml.py create mode 100644 src/mailman/web/Cgi/listinfo.py create mode 100644 src/mailman/web/Cgi/options.py create mode 100644 src/mailman/web/Cgi/private.py create mode 100644 src/mailman/web/Cgi/rmlist.py create mode 100644 src/mailman/web/Cgi/roster.py create mode 100644 src/mailman/web/Cgi/subscribe.py create mode 100644 src/mailman/web/Cgi/wsgi_app.py create mode 100644 src/mailman/web/Gui/Archive.py create mode 100644 src/mailman/web/Gui/Autoresponse.py create mode 100644 src/mailman/web/Gui/Bounce.py create mode 100644 src/mailman/web/Gui/ContentFilter.py create mode 100644 src/mailman/web/Gui/Digest.py create mode 100644 src/mailman/web/Gui/GUIBase.py create mode 100644 src/mailman/web/Gui/General.py create mode 100644 src/mailman/web/Gui/Language.py create mode 100644 src/mailman/web/Gui/Membership.py create mode 100644 src/mailman/web/Gui/NonDigest.py create mode 100644 src/mailman/web/Gui/Passwords.py create mode 100644 src/mailman/web/Gui/Privacy.py create mode 100644 src/mailman/web/Gui/Topics.py create mode 100644 src/mailman/web/Gui/Usenet.py create mode 100644 src/mailman/web/Gui/__init__.py create mode 100644 src/mailman/web/HTMLFormatter.py create mode 100644 src/mailman/web/__init__.py create mode 100644 src/mailman/web/htmlformat.py diff --git a/.bzrignore b/.bzrignore index e63ce55b5..8672761e7 100644 --- a/.bzrignore +++ b/.bzrignore @@ -15,3 +15,4 @@ bin develop-eggs parts ./bin +eggs diff --git a/mailman/Archiver/Archiver.py b/mailman/Archiver/Archiver.py deleted file mode 100644 index d0b9fbd1b..000000000 --- a/mailman/Archiver/Archiver.py +++ /dev/null @@ -1,230 +0,0 @@ -# Copyright (C) 1998-2009 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 . - # USA. - -"""Mixin class for putting new messages in the right place for archival. - -Public archives are separated from private ones. An external archival -mechanism (eg, pipermail) should be pointed to the right places, to do the -archival. -""" - -import os -import errno -import logging - -from cStringIO import StringIO -from string import Template - -from mailman import Mailbox -from mailman import Utils -from mailman.config import config - -log = logging.getLogger('mailman.error') - - - -def makelink(old, new): - try: - os.symlink(old, new) - except OSError, e: - if e.errno <> errno.EEXIST: - raise - -def breaklink(link): - try: - os.unlink(link) - except OSError, e: - if e.errno <> errno.ENOENT: - raise - - - -class Archiver: - # - # Interface to Pipermail. HyperArch.py uses this method to get the - # archive directory for the mailing list - # - def InitVars(self): - # The archive file structure by default is: - # - # archives/ - # private/ - # listname.mbox/ - # listname.mbox - # listname/ - # lots-of-pipermail-stuff - # public/ - # listname.mbox@ -> ../private/listname.mbox - # listname@ -> ../private/listname - # - # IOW, the mbox and pipermail archives are always stored in the - # private archive for the list. This is safe because archives/private - # is always set to o-rx. Public archives have a symlink to get around - # the private directory, pointing directly to the private/listname - # which has o+rx permissions. Private archives do not have the - # symbolic links. - archdir = self.archive_dir(self.fqdn_listname) - omask = os.umask(0) - try: - try: - os.mkdir(archdir+'.mbox', 02775) - except OSError, e: - if e.errno <> errno.EEXIST: - raise - # We also create an empty pipermail archive directory into - # which we'll drop an empty index.html file into. This is so - # that lists that have not yet received a posting have - # /something/ as their index.html, and don't just get a 404. - try: - os.mkdir(archdir, 02775) - except OSError, e: - if e.errno <> errno.EEXIST: - raise - # See if there's an index.html file there already and if not, - # write in the empty archive notice. - indexfile = os.path.join(archdir, 'index.html') - fp = None - try: - fp = open(indexfile) - except IOError, e: - if e.errno <> errno.ENOENT: - raise - omask = os.umask(002) - try: - fp = open(indexfile, 'w') - finally: - os.umask(omask) - fp.write(Utils.maketext( - 'emptyarchive.html', - {'listname': self.real_name, - 'listinfo': self.GetScriptURL('listinfo'), - }, mlist=self)) - if fp: - fp.close() - finally: - os.umask(omask) - - def ArchiveFileName(self): - """The mbox name where messages are left for archive construction.""" - return os.path.join(self.archive_dir() + '.mbox', - self.fqdn_listname + '.mbox') - - def GetBaseArchiveURL(self): - if self.archive_private: - url = self.GetScriptURL('private') + '/index.html' - else: - web_host = config.domains.get(self.host_name, self.host_name) - url = Template(config.PUBLIC_ARCHIVE_URL).safe_substitute( - listname=self.fqdn_listname, - hostname=web_host, - fqdn_listname=self.fqdn_listname, - ) - return url - - def __archive_file(self, afn): - """Open (creating, if necessary) the named archive file.""" - omask = os.umask(002) - try: - return Mailbox.Mailbox(open(afn, 'a+')) - finally: - os.umask(omask) - - # - # old ArchiveMail function, retained under a new name - # for optional archiving to an mbox - # - def __archive_to_mbox(self, post): - """Retain a text copy of the message in an mbox file.""" - try: - afn = self.ArchiveFileName() - mbox = self.__archive_file(afn) - mbox.AppendMessage(post) - mbox.fp.close() - except IOError, msg: - log.error('Archive file access failure:\n\t%s %s', afn, msg) - raise - - def ExternalArchive(self, ar, txt): - cmd = Template(ar).safe_substitute( - listname=self.fqdn_listname, - hostname=self.host_name) - extarch = os.popen(cmd, 'w') - extarch.write(txt) - status = extarch.close() - if status: - log.error('external archiver non-zero exit status: %d\n', - (status & 0xff00) >> 8) - - # - # archiving in real time this is called from list.post(msg) - # - def ArchiveMail(self, msg): - """Store postings in mbox and/or pipermail archive, depending.""" - # Fork so archival errors won't disrupt normal list delivery - if config.ARCHIVE_TO_MBOX == -1: - return - # - # We don't need an extra archiver lock here because we know the list - # itself must be locked. - if config.ARCHIVE_TO_MBOX in (1, 2): - self.__archive_to_mbox(msg) - if config.ARCHIVE_TO_MBOX == 1: - # Archive to mbox only. - return - txt = str(msg) - # should we use the internal or external archiver? - private_p = self.archive_private - if config.PUBLIC_EXTERNAL_ARCHIVER and not private_p: - self.ExternalArchive(config.PUBLIC_EXTERNAL_ARCHIVER, txt) - elif config.PRIVATE_EXTERNAL_ARCHIVER and private_p: - self.ExternalArchive(config.PRIVATE_EXTERNAL_ARCHIVER, txt) - else: - # use the internal archiver - f = StringIO(txt) - import HyperArch - h = HyperArch.HyperArchive(self) - h.processUnixMailbox(f) - h.close() - f.close() - - # - # called from MailList.MailList.Save() - # - def CheckHTMLArchiveDir(self): - # We need to make sure that the archive directory has the right perms - # for public vs private. If it doesn't exist, or some weird - # permissions errors prevent us from stating the directory, it's - # pointless to try to fix the perms, so we just return -scott - if config.ARCHIVE_TO_MBOX == -1: - # Archiving is completely disabled, don't require the skeleton. - return - pubdir = os.path.join(config.PUBLIC_ARCHIVE_FILE_DIR, - self.fqdn_listname) - privdir = self.archive_dir() - pubmbox = pubdir + '.mbox' - privmbox = privdir + '.mbox' - if self.archive_private: - breaklink(pubdir) - breaklink(pubmbox) - else: - # BAW: privdir or privmbox could be nonexistant. We'd get an - # OSError, ENOENT which should be caught and reported properly. - makelink(privdir, pubdir) - # Only make this link if the site has enabled public mbox files - if config.PUBLIC_MBOX: - makelink(privmbox, pubmbox) diff --git a/mailman/Archiver/HyperArch.py b/mailman/Archiver/HyperArch.py deleted file mode 100644 index d9477cc3f..000000000 --- a/mailman/Archiver/HyperArch.py +++ /dev/null @@ -1,1237 +0,0 @@ -# Copyright (C) 1998-2009 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 . - -"""HyperArch: Pipermail archiving for Mailman - - - The Dragon De Monsyne - - TODO: - - Should be able to force all HTML to be regenerated next time the - archive is run, in case a template is changed. - - Run a command to generate tarball of html archives for downloading - (probably in the 'update_dirty_archives' method). -""" - -import os -import re -import sys -import gzip -import time -import errno -import urllib -import logging -import weakref -import binascii - -from email.Charset import Charset -from email.Errors import HeaderParseError -from email.Header import decode_header, make_header -from lazr.config import as_boolean -from locknix.lockfile import Lock -from string import Template - -from mailman import Utils -from mailman import i18n -from mailman.Archiver import HyperDatabase -from mailman.Archiver import pipermail -from mailman.Mailbox import ArchiverMailbox -from mailman.config import config - - -log = logging.getLogger('mailman.error') - -# Set up i18n. Assume the current language has already been set in the caller. -_ = i18n._ - -EMPTYSTRING = '' -NL = '\n' - -# MacOSX has a default stack size that is too small for deeply recursive -# regular expressions. We see this as crashes in the Python test suite when -# running test_re.py and test_sre.py. The fix is to set the stack limit to -# 2048; the general recommendation is to do in the shell before running the -# test suite. But that's inconvenient for a daemon like the qrunner. -# -# AFAIK, this problem only affects the archiver, so we're adding this work -# around to this file (it'll get imported by the bundled pipermail or by the -# bin/arch script. We also only do this on darwin, a.k.a. MacOSX. -if sys.platform == 'darwin': - try: - import resource - except ImportError: - pass - else: - soft, hard = resource.getrlimit(resource.RLIMIT_STACK) - newsoft = min(hard, max(soft, 1024*2048)) - resource.setrlimit(resource.RLIMIT_STACK, (newsoft, hard)) - - - -def html_quote(s, lang=None): - repls = ( ('&', '&'), - ("<", '<'), - (">", '>'), - ('"', '"')) - for thing, repl in repls: - s = s.replace(thing, repl) - return Utils.uncanonstr(s, lang) - - -def url_quote(s): - return urllib.quote(s) - - -def null_to_space(s): - return s.replace('\000', ' ') - - -def sizeof(filename, lang): - try: - size = os.path.getsize(filename) - except OSError, e: - # ENOENT can happen if the .mbox file was moved away or deleted, and - # an explicit mbox file name was given to bin/arch. - if e.errno <> errno.ENOENT: raise - return _('size not available') - if size < 1000: - with i18n.using_language(lang): - out = _(' %(size)i bytes ') - return out - elif size < 1000000: - return ' %d KB ' % (size / 1000) - # GB?? :-) - return ' %d MB ' % (size / 1000000) - - -html_charset = '' - -def CGIescape(arg, lang=None): - if isinstance(arg, unicode): - s = Utils.websafe(arg) - else: - s = Utils.websafe(str(arg)) - return Utils.uncanonstr(s.replace('"', '"'), lang) - -# Parenthesized human name -paren_name_pat = re.compile(r'([(].*[)])') - -# Subject lines preceded with 'Re:' -REpat = re.compile( r"\s*RE\s*(\[\d+\]\s*)?:\s*", re.IGNORECASE) - -# E-mail addresses and URLs in text -emailpat = re.compile(r'([-+,.\w]+@[-+.\w]+)') - -# Argh! This pattern is buggy, and will choke on URLs with GET parameters. -urlpat = re.compile(r'(\w+://[^>)\s]+)') # URLs in text - -# Blank lines -blankpat = re.compile(r'^\s*$') - -# Starting directive -htmlpat = re.compile(r'^\s*\s*$', re.IGNORECASE) -# Ending directive -nohtmlpat = re.compile(r'^\s*\s*$', re.IGNORECASE) -# Match quoted text -quotedpat = re.compile(r'^([>|:]|>)+') - - - -# Like Utils.maketext() but with caching to improve performance. -# -# _templatefilepathcache is used to associate a (templatefile, lang, listname) -# key with the file system path to a template file. This path is the one that -# the Utils.findtext() function has computed is the one to match the values in -# the key tuple. -# -# _templatecache associate a file system path as key with the text -# returned after processing the contents of that file by Utils.findtext() -# -# We keep two caches to reduce the amount of template text kept in memory, -# since the _templatefilepathcache is a many->one mapping and _templatecache -# is a one->one mapping. Imagine 1000 lists all using the same default -# English template. - -_templatefilepathcache = {} -_templatecache = {} - -def quick_maketext(templatefile, dict=None, lang=None, mlist=None): - if mlist is None: - listname = '' - else: - listname = mlist.fqdn_listname - if lang is None: - if mlist is None: - lang = config.mailman.default_language - else: - lang = mlist.preferred_language - cachekey = (templatefile, lang, listname) - filepath = _templatefilepathcache.get(cachekey) - if filepath: - template = _templatecache.get(filepath) - if filepath is None or template is None: - # Use the basic maketext, with defaults to get the raw template - template, filepath = Utils.findtext(templatefile, lang=lang, - raw=True, mlist=mlist) - _templatefilepathcache[cachekey] = filepath - _templatecache[filepath] = template - # Copied from Utils.maketext() - text = template - if dict is not None: - try: - try: - text = Template(template).safe_substitute(**dict) - except UnicodeError: - # Try again after coercing the template to unicode - utemplate = unicode(template, - Utils.GetCharSet(lang), - 'replace') - text = Template(utemplate).safe_substitute(**dict) - except (TypeError, ValueError): - # The template is really screwed up - pass - # Make sure the text is in the given character set, or html-ify any bogus - # characters. - return Utils.uncanonstr(text, lang) - - - -# Note: I'm overriding most, if not all of the pipermail Article class -# here -ddm -# The Article class encapsulates a single posting. The attributes are: -# -# sequence : Sequence number, unique for each article in a set of archives -# subject : Subject -# datestr : The posting date, in human-readable format -# date : The posting date, in purely numeric format -# fromdate : The posting date, in `unixfrom' format -# headers : Any other headers of interest -# author : The author's name (and possibly organization) -# email : The author's e-mail address -# msgid : A unique message ID -# in_reply_to : If !="", this is the msgid of the article being replied to -# references: A (possibly empty) list of msgid's of earlier articles in -# the thread -# body : A list of strings making up the message body - -class Article(pipermail.Article): - __super_init = pipermail.Article.__init__ - __super_set_date = pipermail.Article._set_date - - _last_article_time = time.time() - - def __init__(self, message=None, sequence=0, keepHeaders=[], - lang=config.mailman.default_language, mlist=None): - self.__super_init(message, sequence, keepHeaders) - self.prev = None - self.next = None - # Trim Re: from the subject line - i = 0 - while i != -1: - result = REpat.match(self.subject) - if result: - i = result.end(0) - self.subject = self.subject[i:] - else: - i = -1 - # Useful to keep around - self._lang = lang - self._mlist = mlist - - if as_boolean(config.archiver.pipermail.obscure_email_addresses): - # Avoid i18n side-effects. Note that the language for this - # article (for this list) could be different from the site-wide - # preferred language, so we need to ensure no side-effects will - # occur. Think what happens when executing bin/arch. - with i18n.using_language(lang): - if self.author == self.email: - self.author = self.email = re.sub('@', _(' at '), - self.email) - else: - self.email = re.sub('@', _(' at '), self.email) - # Snag the content-* headers. RFC 1521 states that their values are - # case insensitive. - ctype = message.get('Content-Type', 'text/plain') - cenc = message.get('Content-Transfer-Encoding', '') - self.ctype = ctype.lower() - self.cenc = cenc.lower() - self.decoded = {} - cset = Utils.GetCharSet(mlist.preferred_language) - cset_out = Charset(cset).output_charset or cset - charset = message.get_content_charset(cset_out) - if charset: - charset = charset.lower().strip() - if charset[0]=='"' and charset[-1]=='"': - charset = charset[1:-1] - if charset[0]=="'" and charset[-1]=="'": - charset = charset[1:-1] - try: - body = message.get_payload(decode=True) - except binascii.Error: - body = None - if body and charset != Utils.GetCharSet(self._lang): - # decode body - try: - body = unicode(body, charset) - except (UnicodeError, LookupError): - body = None - if body: - self.body = [l + "\n" for l in body.splitlines()] - - self.decode_headers() - - def __getstate__(self): - d = self.__dict__.copy() - # We definitely don't want to pickle the MailList instance, so just - # pickle a reference to it. - if d.has_key('_mlist'): - mlist = d['_mlist'] - del d['_mlist'] - else: - mlist = None - if mlist: - d['__listname'] = self._mlist.fqdn_listname - else: - d['__listname'] = None - # Delete a few other things we don't want in the pickle - for attr in ('prev', 'next', 'body'): - if d.has_key(attr): - del d[attr] - d['body'] = [] - return d - - def __setstate__(self, d): - # For loading older Articles via pickle. All this stuff was added - # when Simone Piunni and Tokio Kikuchi i18n'ified Pipermail. See SF - # patch #594771. - self.__dict__ = d - listname = d.get('__listname') - if listname: - del d['__listname'] - d['_mlist'] = config.db.list_manager.get(listname) - if not d.has_key('_lang'): - if hasattr(self, '_mlist'): - self._lang = self._mlist.preferred_language - else: - self._lang = config.mailman.default_language - if not d.has_key('cenc'): - self.cenc = None - if not d.has_key('decoded'): - self.decoded = {} - - def setListIfUnset(self, mlist): - if getattr(self, '_mlist', None) is None: - self._mlist = mlist - - def quote(self, buf): - return html_quote(buf, self._lang) - - def decode_headers(self): - """MIME-decode headers. - - If the email, subject, or author attributes contain non-ASCII - characters using the encoded-word syntax of RFC 2047, decoded versions - of those attributes are placed in the self.decoded (a dictionary). - - If the list's charset differs from the header charset, an attempt is - made to decode the headers as Unicode. If that fails, they are left - undecoded. - """ - author = self.decode_charset(self.author) - subject = self.decode_charset(self.subject) - if author: - self.decoded['author'] = author - email = self.decode_charset(self.email) - if email: - self.decoded['email'] = email - if subject: - if as_boolean(config.archiver.pipermail.obscure_email_addresses): - with i18n.using_language(self._lang): - atmark = _(' at ') - subject = re.sub(r'([-+,.\w]+)@([-+.\w]+)', - '\g<1>' + atmark + '\g<2>', subject) - self.decoded['subject'] = subject - self.decoded['stripped'] = self.strip_subject(subject or self.subject) - - def strip_subject(self, subject): - # Strip subject_prefix and Re: for subject sorting - # This part was taken from CookHeaders.py (TK) - prefix = self._mlist.subject_prefix.strip() - if prefix: - prefix_pat = re.escape(prefix) - prefix_pat = '%'.join(prefix_pat.split(r'\%')) - prefix_pat = re.sub(r'%\d*d', r'\s*\d+\s*', prefix_pat) - subject = re.sub(prefix_pat, '', subject) - subject = subject.lstrip() - strip_pat = re.compile('^((RE|AW|SV|VS)(\[\d+\])?:\s*)+', re.I) - stripped = strip_pat.sub('', subject) - return stripped - - def decode_charset(self, field): - # TK: This function was rewritten for unifying to Unicode. - # Convert 'field' into Unicode one line string. - try: - pairs = decode_header(field) - ustr = make_header(pairs).__unicode__() - except (LookupError, UnicodeError, ValueError, HeaderParseError): - # assume list's language - cset = Utils.GetCharSet(self._mlist.preferred_language) - if cset == 'us-ascii': - cset = 'iso-8859-1' # assume this for English list - ustr = unicode(field, cset, 'replace') - return u''.join(ustr.splitlines()) - - def as_html(self): - d = self.__dict__.copy() - # avoid i18n side-effects - with i18n.using_language(self._lang): - d["prev"], d["prev_wsubj"] = self._get_prev() - d["next"], d["next_wsubj"] = self._get_next() - - d["email_html"] = self.quote(self.email) - d["title"] = self.quote(self.subject) - d["subject_html"] = self.quote(self.subject) - d["subject_url"] = url_quote(self.subject) - d["in_reply_to_url"] = url_quote(self.in_reply_to) - if as_boolean(config.archiver.pipermail.obscure_email_addresses): - # Point the mailto url back to the list - author = re.sub('@', _(' at '), self.author) - emailurl = self._mlist.posting_address - else: - author = self.author - emailurl = self.email - d["author_html"] = self.quote(author) - d["email_url"] = url_quote(emailurl) - d["datestr_html"] = self.quote(i18n.ctime(int(self.date))) - d["body"] = self._get_body() - d['listurl'] = self._mlist.script_url('listinfo') - d['listname'] = self._mlist.real_name - d['encoding'] = '' - charset = Utils.GetCharSet(self._lang) - d["encoding"] = html_charset % charset - - self._add_decoded(d) - return quick_maketext( - 'article.html', d, - lang=self._lang, mlist=self._mlist) - - def _get_prev(self): - """Return the href and subject for the previous message""" - if self.prev: - subject = self._get_subject_enc(self.prev) - prev = ('' - % (url_quote(self.prev.filename))) - prev_wsubj = ('
  • ' + _('Previous message (by thread):') + - ' %s\n
  • ' - % (url_quote(self.prev.filename), - self.quote(subject))) - else: - prev = prev_wsubj = "" - return prev, prev_wsubj - - def _get_subject_enc(self, art): - """Return the subject of art, decoded if possible. - - If the charset of the current message and art match and the - article's subject is encoded, decode it. - """ - return art.decoded.get('subject', art.subject) - - def _get_next(self): - """Return the href and subject for the previous message""" - if self.next: - subject = self._get_subject_enc(self.next) - next = ('' - % (url_quote(self.next.filename))) - next_wsubj = ('
  • ' + _('Next message (by thread):') + - ' %s\n
  • ' - % (url_quote(self.next.filename), - self.quote(subject))) - else: - next = next_wsubj = "" - return next, next_wsubj - - _rx_quote = re.compile('=([A-F0-9][A-F0-9])') - _rx_softline = re.compile('=[ \t]*$') - - def _get_body(self): - """Return the message body ready for HTML, decoded if necessary""" - try: - body = self.html_body - except AttributeError: - body = self.body - return null_to_space(EMPTYSTRING.join(body)) - - def _add_decoded(self, d): - """Add encoded-word keys to HTML output""" - for src, dst in (('author', 'author_html'), - ('email', 'email_html'), - ('subject', 'subject_html'), - ('subject', 'title')): - if self.decoded.has_key(src): - d[dst] = self.quote(self.decoded[src]) - - def as_text(self): - d = self.__dict__.copy() - # We need to guarantee a valid From_ line, even if there are - # bososities in the headers. - if not d.get('fromdate', '').strip(): - d['fromdate'] = time.ctime(time.time()) - if not d.get('email', '').strip(): - d['email'] = 'bogus@does.not.exist.com' - if not d.get('datestr', '').strip(): - d['datestr'] = time.ctime(time.time()) - # - headers = ['From %(email)s %(fromdate)s', - 'From: %(email)s (%(author)s)', - 'Date: %(datestr)s', - 'Subject: %(subject)s'] - if d['_in_reply_to']: - headers.append('In-Reply-To: %(_in_reply_to)s') - if d['_references']: - headers.append('References: %(_references)s') - if d['_message_id']: - headers.append('Message-ID: %(_message_id)s') - body = EMPTYSTRING.join(self.body) - cset = Utils.GetCharSet(self._lang) - # Coerce the body to Unicode and replace any invalid characters. - if not isinstance(body, unicode): - body = unicode(body, cset, 'replace') - if as_boolean(config.archiver.pipermail.obscure_email_addresses): - with i18n.using_language(self._lang): - atmark = _(' at ') - body = re.sub(r'([-+,.\w]+)@([-+.\w]+)', - '\g<1>' + atmark + '\g<2>', body) - # Return body to character set of article. - body = body.encode(cset, 'replace') - return NL.join(headers) % d + '\n\n' + body + '\n' - - def _set_date(self, message): - self.__super_set_date(message) - self.fromdate = time.ctime(int(self.date)) - - def loadbody_fromHTML(self,fileobj): - self.body = [] - begin = 0 - while 1: - line = fileobj.readline() - if not line: - break - if not begin: - if line.strip() == '': - begin = 1 - continue - if line.strip() == '': - break - self.body.append(line) - - def finished_update_article(self): - self.body = [] - try: - del self.html_body - except AttributeError: - pass - - -class HyperArchive(pipermail.T): - __super_init = pipermail.T.__init__ - __super_update_archive = pipermail.T.update_archive - __super_update_dirty_archives = pipermail.T.update_dirty_archives - __super_add_article = pipermail.T.add_article - - # some defaults - DIRMODE = 02775 - FILEMODE = 0660 - - VERBOSE = 0 - DEFAULTINDEX = 'thread' - ARCHIVE_PERIOD = 'month' - - THREADLAZY = 0 - THREADLEVELS = 3 - - ALLOWHTML = 1 # "Lines between " handled as is. - SHOWHTML = 0 # Eg, nuke leading whitespace in html manner. - IQUOTES = 1 # Italicize quoted text. - SHOWBR = 0 # Add
    onto every line - - def __init__(self, maillist): - # can't init the database while other processes are writing to it! - dir = maillist.archive_dir() - db = HyperDatabase.HyperDatabase(dir, maillist) - self.__super_init(dir, reload=1, database=db) - - self.maillist = maillist - self._lock_file = None - self.lang = maillist.preferred_language - self.charset = Utils.GetCharSet(maillist.preferred_language) - - if hasattr(self.maillist,'archive_volume_frequency'): - if self.maillist.archive_volume_frequency == 0: - self.ARCHIVE_PERIOD='year' - elif self.maillist.archive_volume_frequency == 2: - self.ARCHIVE_PERIOD='quarter' - elif self.maillist.archive_volume_frequency == 3: - self.ARCHIVE_PERIOD='week' - elif self.maillist.archive_volume_frequency == 4: - self.ARCHIVE_PERIOD='day' - else: - self.ARCHIVE_PERIOD='month' - - yre = r'(?P[0-9]{4,4})' - mre = r'(?P[01][0-9])' - dre = r'(?P[0123][0-9])' - self._volre = { - 'year': '^' + yre + '$', - 'quarter': '^' + yre + r'q(?P[1234])$', - 'month': '^' + yre + r'-(?P[a-zA-Z]+)$', - 'week': r'^Week-of-Mon-' + yre + mre + dre, - 'day': '^' + yre + mre + dre + '$' - } - - def _makeArticle(self, msg, sequence): - return Article(msg, sequence, - lang=self.maillist.preferred_language, - mlist=self.maillist) - - def html_foot(self): - # avoid i18n side-effects - mlist = self.maillist - # Convenience - def quotetime(s): - return html_quote(i18n.ctime(s), self.lang) - with i18n.using_language(mlist.preferred_language): - d = {"lastdate": quotetime(self.lastdate), - "archivedate": quotetime(self.archivedate), - "listinfo": mlist.script_url('listinfo'), - "version": self.version, - } - i = {"thread": _("thread"), - "subject": _("subject"), - "author": _("author"), - "date": _("date") - } - for t in i.keys(): - cap = t[0].upper() + t[1:] - if self.type == cap: - d["%s_ref" % (t)] = "" - else: - d["%s_ref" % (t)] = ('[ %s ]' - % (t, i[t])) - return quick_maketext( - 'archidxfoot.html', d, - mlist=mlist) - - def html_head(self): - # avoid i18n side-effects - mlist = self.maillist - # Convenience - def quotetime(s): - return html_quote(i18n.ctime(s), self.lang) - with i18n.using_language(mlist.preferred_language): - d = {"listname": html_quote(mlist.real_name, self.lang), - "archtype": self.type, - "archive": self.volNameToDesc(self.archive), - "listinfo": mlist.script_url('listinfo'), - "firstdate": quotetime(self.firstdate), - "lastdate": quotetime(self.lastdate), - "size": self.size, - } - i = {"thread": _("thread"), - "subject": _("subject"), - "author": _("author"), - "date": _("date"), - } - for t in i.keys(): - cap = t[0].upper() + t[1:] - if self.type == cap: - d["%s_ref" % (t)] = "" - d["archtype"] = i[t] - else: - d["%s_ref" % (t)] = ('[ %s ]' - % (t, i[t])) - if self.charset: - d["encoding"] = html_charset % self.charset - else: - d["encoding"] = "" - return quick_maketext( - 'archidxhead.html', d, - mlist=mlist) - - def html_TOC(self): - mlist = self.maillist - listname = mlist.fqdn_listname - mbox = os.path.join(mlist.archive_dir()+'.mbox', listname+'.mbox') - d = {"listname": mlist.real_name, - "listinfo": mlist.script_url('listinfo'), - "fullarch": '../%s.mbox/%s.mbox' % (listname, listname), - "size": sizeof(mbox, mlist.preferred_language), - 'meta': '', - } - # Avoid i18n side-effects - with i18n.using_language(mlist.preferred_language): - if not self.archives: - d["noarchive_msg"] = _( - '

    Currently, there are no archives.

    ') - d["archive_listing_start"] = "" - d["archive_listing_end"] = "" - d["archive_listing"] = "" - else: - d["noarchive_msg"] = "" - d["archive_listing_start"] = quick_maketext( - 'archliststart.html', - lang=mlist.preferred_language, - mlist=mlist) - d["archive_listing_end"] = quick_maketext( - 'archlistend.html', - mlist=mlist) - - accum = [] - for a in self.archives: - accum.append(self.html_TOC_entry(a)) - d["archive_listing"] = EMPTYSTRING.join(accum) - # The TOC is always in the charset of the list's preferred language - d['meta'] += html_charset % Utils.GetCharSet(mlist.preferred_language) - # The site can disable public access to the mbox file. - if as_boolean(config.archiver.pipermail.public_mbox): - template = 'archtoc.html' - else: - template = 'archtocnombox.html' - return quick_maketext(template, d, mlist=mlist) - - def html_TOC_entry(self, arch): - # Check to see if the archive is gzip'd or not - txtfile = os.path.join(self.maillist.archive_dir(), arch + '.txt') - gzfile = txtfile + '.gz' - # which exists? .txt.gz first, then .txt - if os.path.exists(gzfile): - file = gzfile - url = arch + '.txt.gz' - templ = '[ ' + _('Gzip\'d Text%(sz)s') \ - + ']' - elif os.path.exists(txtfile): - file = txtfile - url = arch + '.txt' - templ = '[ ' + _('Text%(sz)s') + ']' - else: - # neither found? - file = None - # in Python 1.5.2 we have an easy way to get the size - if file: - textlink = templ % { - 'url': url, - 'sz' : sizeof(file, self.maillist.preferred_language) - } - else: - # there's no archive file at all... hmmm. - textlink = '' - return quick_maketext( - 'archtocentry.html', - {'archive': arch, - 'archivelabel': self.volNameToDesc(arch), - 'textlink': textlink - }, - mlist=self.maillist) - - def GetArchLock(self): - if self._lock_file: - return 1 - self._lock_file = Lock( - os.path.join(config.LOCK_DIR, - self.maillist.fqdn_listname + '-arch.lock')) - try: - self._lock_file.lock(timeout=0.5) - except lockfile.TimeOutError: - return 0 - return 1 - - def DropArchLock(self): - if self._lock_file: - self._lock_file.unlock(unconditionally=1) - self._lock_file = None - - def processListArch(self): - name = self.maillist.ArchiveFileName() - wname= name+'.working' - ename= name+'.err_unarchived' - try: - os.stat(name) - except (IOError,os.error): - #no archive file, nothin to do -ddm - return - - #see if arch is locked here -ddm - if not self.GetArchLock(): - #another archiver is running, nothing to do. -ddm - return - - #if the working file is still here, the archiver may have - # crashed during archiving. Save it, log an error, and move on. - try: - wf = open(wname) - log.error('Archive working file %s present. ' - 'Check %s for possibly unarchived msgs', - wname, ename) - omask = os.umask(007) - try: - ef = open(ename, 'a+') - finally: - os.umask(omask) - ef.seek(1,2) - if ef.read(1) <> '\n': - ef.write('\n') - ef.write(wf.read()) - ef.close() - wf.close() - os.unlink(wname) - except IOError: - pass - os.rename(name,wname) - archfile = open(wname) - self.processUnixMailbox(archfile) - archfile.close() - os.unlink(wname) - self.DropArchLock() - - def get_filename(self, article): - return '%06i.html' % (article.sequence,) - - def get_archives(self, article): - """Return a list of indexes where the article should be filed. - A string can be returned if the list only contains one entry, - and the empty list is legal.""" - res = self.dateToVolName(float(article.date)) - self.message(_("figuring article archives\n")) - self.message(res + "\n") - return res - - def volNameToDesc(self, volname): - volname = volname.strip() - # Don't make these module global constants since we have to runtime - # translate them anyway. - monthdict = [ - '', - _('January'), _('February'), _('March'), _('April'), - _('May'), _('June'), _('July'), _('August'), - _('September'), _('October'), _('November'), _('December') - ] - for each in self._volre.keys(): - match = re.match(self._volre[each], volname) - # Let ValueErrors percolate up - if match: - year = int(match.group('year')) - if each == 'quarter': - d =["", _("First"), _("Second"), _("Third"), _("Fourth") ] - ord = d[int(match.group('quarter'))] - return _("%(ord)s quarter %(year)i") - elif each == 'month': - monthstr = match.group('month').lower() - for i in range(1, 13): - monthname = time.strftime("%B", (1999,i,1,0,0,0,0,1,0)) - if monthstr.lower() == monthname.lower(): - month = monthdict[i] - return _("%(month)s %(year)i") - raise ValueError, "%s is not a month!" % monthstr - elif each == 'week': - month = monthdict[int(match.group("month"))] - day = int(match.group("day")) - return _("The Week Of Monday %(day)i %(month)s %(year)i") - elif each == 'day': - month = monthdict[int(match.group("month"))] - day = int(match.group("day")) - return _("%(day)i %(month)s %(year)i") - else: - return match.group('year') - raise ValueError, "%s is not a valid volname" % volname - -# The following two methods should be inverses of each other. -ddm - - def dateToVolName(self,date): - datetuple=time.localtime(date) - if self.ARCHIVE_PERIOD=='year': - return time.strftime("%Y",datetuple) - elif self.ARCHIVE_PERIOD=='quarter': - if datetuple[1] in [1,2,3]: - return time.strftime("%Yq1",datetuple) - elif datetuple[1] in [4,5,6]: - return time.strftime("%Yq2",datetuple) - elif datetuple[1] in [7,8,9]: - return time.strftime("%Yq3",datetuple) - else: - return time.strftime("%Yq4",datetuple) - elif self.ARCHIVE_PERIOD == 'day': - return time.strftime("%Y%m%d", datetuple) - elif self.ARCHIVE_PERIOD == 'week': - # Reconstruct "seconds since epoch", and subtract weekday - # multiplied by the number of seconds in a day. - monday = time.mktime(datetuple) - datetuple[6] * 24 * 60 * 60 - # Build a new datetuple from this "seconds since epoch" value - datetuple = time.localtime(monday) - return time.strftime("Week-of-Mon-%Y%m%d", datetuple) - # month. -ddm - else: - return time.strftime("%Y-%B",datetuple) - - - def volNameToDate(self, volname): - volname = volname.strip() - for each in self._volre.keys(): - match = re.match(self._volre[each],volname) - if match: - year = int(match.group('year')) - month = 1 - day = 1 - if each == 'quarter': - q = int(match.group('quarter')) - month = (q * 3) - 2 - elif each == 'month': - monthstr = match.group('month').lower() - m = [] - for i in range(1,13): - m.append( - time.strftime("%B",(1999,i,1,0,0,0,0,1,0)).lower()) - try: - month = m.index(monthstr) + 1 - except ValueError: - pass - elif each == 'week' or each == 'day': - month = int(match.group("month")) - day = int(match.group("day")) - try: - return time.mktime((year,month,1,0,0,0,0,1,-1)) - except OverflowError: - return 0.0 - return 0.0 - - def sortarchives(self): - def sf(a, b): - al = self.volNameToDate(a) - bl = self.volNameToDate(b) - if al > bl: - return 1 - elif al < bl: - return -1 - else: - return 0 - if self.ARCHIVE_PERIOD in ('month','year','quarter'): - self.archives.sort(sf) - else: - self.archives.sort() - self.archives.reverse() - - def message(self, msg): - if self.VERBOSE: - f = sys.stderr - f.write(msg) - if msg[-1:] != '\n': - f.write('\n') - f.flush() - - def open_new_archive(self, archive, archivedir): - index_html = os.path.join(archivedir, 'index.html') - try: - os.unlink(index_html) - except: - pass - os.symlink(self.DEFAULTINDEX+'.html',index_html) - - def write_index_header(self): - self.depth=0 - print self.html_head() - if not self.THREADLAZY and self.type=='Thread': - self.message(_("Computing threaded index\n")) - self.updateThreadedIndex() - - def write_index_footer(self): - for i in range(self.depth): - print '' - print self.html_foot() - - def write_index_entry(self, article): - subject = self.get_header("subject", article) - author = self.get_header("author", article) - if as_boolean(config.archiver.pipermail.obscure_email_addresses): - try: - author = re.sub('@', _(' at '), author) - except UnicodeError: - # Non-ASCII author contains '@' ... no valid email anyway - pass - subject = CGIescape(subject, self.lang) - author = CGIescape(author, self.lang) - - d = { - 'filename': urllib.quote(article.filename), - 'subject': subject, - 'sequence': article.sequence, - 'author': author - } - print quick_maketext( - 'archidxentry.html', d, - mlist=self.maillist) - - def get_header(self, field, article): - # if we have no decoded header, return the encoded one - result = article.decoded.get(field) - if result is None: - return getattr(article, field) - # otherwise, the decoded one will be Unicode - return result - - def write_threadindex_entry(self, article, depth): - if depth < 0: - self.message('depth<0') - depth = 0 - if depth > self.THREADLEVELS: - depth = self.THREADLEVELS - if depth < self.depth: - for i in range(self.depth-depth): - print '' - elif depth > self.depth: - for i in range(depth-self.depth): - print '
      ' - print '' % (depth, article.threadKey) - self.depth = depth - self.write_index_entry(article) - - def write_TOC(self): - self.sortarchives() - omask = os.umask(002) - try: - toc = open(os.path.join(self.basedir, 'index.html'), 'w') - finally: - os.umask(omask) - toc.write(self.html_TOC()) - toc.close() - - def write_article(self, index, article, path): - # called by add_article - omask = os.umask(002) - try: - f = open(path, 'w') - finally: - os.umask(omask) - f.write(article.as_html()) - f.close() - - # Write the text article to the text archive. - path = os.path.join(self.basedir, "%s.txt" % index) - omask = os.umask(002) - try: - f = open(path, 'a+') - finally: - os.umask(omask) - f.write(article.as_text()) - f.close() - - def update_archive(self, archive): - self.__super_update_archive(archive) - # only do this if the gzip module was imported globally, and - # gzip'ing was enabled via Defaults.GZIP_ARCHIVE_TXT_FILES. See - # above. - if gzip: - archz = None - archt = None - txtfile = os.path.join(self.basedir, '%s.txt' % archive) - gzipfile = os.path.join(self.basedir, '%s.txt.gz' % archive) - oldgzip = os.path.join(self.basedir, '%s.old.txt.gz' % archive) - try: - # open the plain text file - archt = open(txtfile) - except IOError: - return - try: - os.rename(gzipfile, oldgzip) - archz = gzip.open(oldgzip) - except (IOError, RuntimeError, os.error): - pass - try: - ou = os.umask(002) - newz = gzip.open(gzipfile, 'w') - finally: - # XXX why is this a finally? - os.umask(ou) - if archz: - newz.write(archz.read()) - archz.close() - os.unlink(oldgzip) - # XXX do we really need all this in a try/except? - try: - newz.write(archt.read()) - newz.close() - archt.close() - except IOError: - pass - os.unlink(txtfile) - - _skip_attrs = ('maillist', '_lock_file', 'charset') - - def getstate(self): - d={} - for each in self.__dict__.keys(): - if not (each in self._skip_attrs - or each.upper() == each): - d[each] = self.__dict__[each] - return d - - # Add tags around URLs and e-mail addresses. - - def __processbody_URLquote(self, lines): - # XXX a lot to do here: - # 1. use lines directly, rather than source and dest - # 2. make it clearer - # 3. make it faster - # TK: Prepare for unicode obscure. - atmark = _(' at ') - if lines and isinstance(lines[0], unicode): - atmark = unicode(atmark, Utils.GetCharSet(self.lang), 'replace') - source = lines[:] - dest = lines - last_line_was_quoted = 0 - for i in xrange(0, len(source)): - Lorig = L = source[i] - prefix = suffix = "" - if L is None: - continue - # Italicise quoted text - if self.IQUOTES: - quoted = quotedpat.match(L) - if quoted is None: - last_line_was_quoted = 0 - else: - quoted = quoted.end(0) - prefix = CGIescape(L[:quoted], self.lang) + '' - suffix = '' - if self.SHOWHTML: - suffix += '
      ' - if not last_line_was_quoted: - prefix = '
      ' + prefix - L = L[quoted:] - last_line_was_quoted = 1 - # Check for an e-mail address - L2 = "" - jr = emailpat.search(L) - kr = urlpat.search(L) - while jr is not None or kr is not None: - if jr == None: - j = -1 - else: - j = jr.start(0) - if kr is None: - k = -1 - else: - k = kr.start(0) - if j != -1 and (j < k or k == -1): - text = jr.group(1) - length = len(text) - if as_boolean( - config.archiver.pipermail.obscure_email_addresses): - text = re.sub('@', atmark, text) - URL = self.maillist.script_url('listinfo') - else: - URL = 'mailto:' + text - pos = j - elif k != -1 and (j > k or j == -1): - text = URL = kr.group(1) - length = len(text) - pos = k - else: # j==k - raise ValueError, "j==k: This can't happen!" - #length = len(text) - #self.message("URL: %s %s %s \n" - # % (CGIescape(L[:pos]), URL, CGIescape(text))) - L2 += '%s
      %s' % ( - CGIescape(L[:pos], self.lang), - html_quote(URL), CGIescape(text, self.lang)) - L = L[pos+length:] - jr = emailpat.search(L) - kr = urlpat.search(L) - if jr is None and kr is None: - L = CGIescape(L, self.lang) - L = prefix + L2 + L + suffix - source[i] = None - dest[i] = L - - # Perform Hypermail-style processing of directives - # in message bodies. Lines between and will be written - # out precisely as they are; other lines will be passed to func2 - # for further processing . - - def __processbody_HTML(self, lines): - # XXX need to make this method modify in place - source = lines[:] - dest = lines - l = len(source) - i = 0 - while i < l: - while i < l and htmlpat.match(source[i]) is None: - i = i + 1 - if i < l: - source[i] = None - i = i + 1 - while i < l and nohtmlpat.match(source[i]) is None: - dest[i], source[i] = source[i], None - i = i + 1 - if i < l: - source[i] = None - i = i + 1 - - def format_article(self, article): - # called from add_article - # TBD: Why do the HTML formatting here and keep it in the - # pipermail database? It makes more sense to do the html - # formatting as the article is being written as html and toss - # the data after it has been written to the archive file. - lines = filter(None, article.body) - # Handle directives - if self.ALLOWHTML: - self.__processbody_HTML(lines) - self.__processbody_URLquote(lines) - if not self.SHOWHTML and lines: - lines.insert(0, '
      ')
      -            lines.append('
      ') - else: - # Do fancy formatting here - if self.SHOWBR: - lines = map(lambda x:x + "
      ", lines) - else: - for i in range(0, len(lines)): - s = lines[i] - if s[0:1] in ' \t\n': - lines[i] = '

      ' + s - article.html_body = lines - return article - - def update_article(self, arcdir, article, prev, next): - seq = article.sequence - filename = os.path.join(arcdir, article.filename) - self.message(_('Updating HTML for article %(seq)s')) - try: - f = open(filename) - article.loadbody_fromHTML(f) - f.close() - except IOError, e: - if e.errno <> errno.ENOENT: raise - self.message(_('article file %(filename)s is missing!')) - article.prev = prev - article.next = next - omask = os.umask(002) - try: - f = open(filename, 'w') - finally: - os.umask(omask) - f.write(article.as_html()) - f.close() diff --git a/mailman/Archiver/HyperDatabase.py b/mailman/Archiver/HyperDatabase.py deleted file mode 100644 index 49928d7b3..000000000 --- a/mailman/Archiver/HyperDatabase.py +++ /dev/null @@ -1,339 +0,0 @@ -# Copyright (C) 1998-2009 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 . - -# -# site modules -# -import os -import marshal -import time -import errno - -# -# package/project modules -# -import pipermail -from locknix import lockfile - -CACHESIZE = pipermail.CACHESIZE - -try: - import cPickle - pickle = cPickle -except ImportError: - import pickle - -# -# we're using a python dict in place of -# of bsddb.btree database. only defining -# the parts of the interface used by class HyperDatabase -# only one thing can access this at a time. -# -class DumbBTree: - """Stores pickles of Article objects - - This dictionary-like object stores pickles of all the Article - objects. The object itself is stored using marshal. It would be - much simpler, and probably faster, to store the actual objects in - the DumbBTree and pickle it. - - TBD: Also needs a more sensible name, like IteratableDictionary or - SortedDictionary. - """ - - def __init__(self, path): - self.current_index = 0 - self.path = path - self.lockfile = lockfile.Lock(self.path + ".lock") - self.lock() - self.__dirty = 0 - self.dict = {} - self.sorted = [] - self.load() - - def __repr__(self): - return "DumbBTree(%s)" % self.path - - def __sort(self, dirty=None): - if self.__dirty == 1 or dirty: - self.sorted = self.dict.keys() - self.sorted.sort() - self.__dirty = 0 - - def lock(self): - self.lockfile.lock() - - def unlock(self): - try: - self.lockfile.unlock() - except lockfile.NotLockedError: - pass - - def __delitem__(self, item): - # if first hasn't been called, we can skip the sort - if self.current_index == 0: - del self.dict[item] - self.__dirty = 1 - return - try: - ci = self.sorted[self.current_index] - except IndexError: - ci = None - if ci == item: - try: - ci = self.sorted[self.current_index + 1] - except IndexError: - ci = None - del self.dict[item] - self.__sort(dirty=1) - if ci is not None: - self.current_index = self.sorted.index(ci) - else: - self.current_index = self.current_index + 1 - - def clear(self): - # bulk clearing much faster than deleting each item, esp. with the - # implementation of __delitem__() above :( - self.dict = {} - - def first(self): - self.__sort() # guarantee that the list is sorted - if not self.sorted: - raise KeyError - else: - key = self.sorted[0] - self.current_index = 1 - return key, self.dict[key] - - def last(self): - if not self.sorted: - raise KeyError - else: - key = self.sorted[-1] - self.current_index = len(self.sorted) - 1 - return key, self.dict[key] - - def next(self): - try: - key = self.sorted[self.current_index] - except IndexError: - raise KeyError - self.current_index = self.current_index + 1 - return key, self.dict[key] - - def has_key(self, key): - return self.dict.has_key(key) - - def set_location(self, loc): - if not self.dict.has_key(loc): - raise KeyError - self.current_index = self.sorted.index(loc) - - def __getitem__(self, item): - return self.dict[item] - - def __setitem__(self, item, val): - # if first hasn't been called, then we don't need to worry - # about sorting again - if self.current_index == 0: - self.dict[item] = val - self.__dirty = 1 - return - try: - current_item = self.sorted[self.current_index] - except IndexError: - current_item = item - self.dict[item] = val - self.__sort(dirty=1) - self.current_index = self.sorted.index(current_item) - - def __len__(self): - return len(self.sorted) - - def load(self): - try: - fp = open(self.path) - try: - self.dict = marshal.load(fp) - finally: - fp.close() - except IOError, e: - if e.errno <> errno.ENOENT: raise - pass - except EOFError: - pass - else: - self.__sort(dirty=1) - - def close(self): - omask = os.umask(007) - try: - fp = open(self.path, 'w') - finally: - os.umask(omask) - fp.write(marshal.dumps(self.dict)) - fp.close() - self.unlock() - - -# this is lifted straight out of pipermail with -# the bsddb.btree replaced with above class. -# didn't use inheritance because of all the -# __internal stuff that needs to be here -scott -# -class HyperDatabase(pipermail.Database): - __super_addArticle = pipermail.Database.addArticle - - def __init__(self, basedir, mlist): - self.__cache = {} - self.__currentOpenArchive = None # The currently open indices - self._mlist = mlist - self.basedir = os.path.expanduser(basedir) - # Recently added articles, indexed only by message ID - self.changed={} - - def firstdate(self, archive): - self.__openIndices(archive) - date = 'None' - try: - datekey, msgid = self.dateIndex.first() - date = time.asctime(time.localtime(float(datekey[0]))) - except KeyError: - pass - return date - - def lastdate(self, archive): - self.__openIndices(archive) - date = 'None' - try: - datekey, msgid = self.dateIndex.last() - date = time.asctime(time.localtime(float(datekey[0]))) - except KeyError: - pass - return date - - def numArticles(self, archive): - self.__openIndices(archive) - return len(self.dateIndex) - - def addArticle(self, archive, article, subject=None, author=None, - date=None): - self.__openIndices(archive) - self.__super_addArticle(archive, article, subject, author, date) - - def __openIndices(self, archive): - if self.__currentOpenArchive == archive: - return - self.__closeIndices() - arcdir = os.path.join(self.basedir, 'database') - omask = os.umask(0) - try: - try: - os.mkdir(arcdir, 02770) - except OSError, e: - if e.errno <> errno.EEXIST: raise - finally: - os.umask(omask) - for i in ('date', 'author', 'subject', 'article', 'thread'): - t = DumbBTree(os.path.join(arcdir, archive + '-' + i)) - setattr(self, i + 'Index', t) - self.__currentOpenArchive = archive - - def __closeIndices(self): - for i in ('date', 'author', 'subject', 'thread', 'article'): - attr = i + 'Index' - if hasattr(self, attr): - index = getattr(self, attr) - if i == 'article': - if not hasattr(self, 'archive_length'): - self.archive_length = {} - l = len(index) - self.archive_length[self.__currentOpenArchive] = l - index.close() - delattr(self, attr) - self.__currentOpenArchive = None - - def close(self): - self.__closeIndices() - - def hasArticle(self, archive, msgid): - self.__openIndices(archive) - return self.articleIndex.has_key(msgid) - - def setThreadKey(self, archive, key, msgid): - self.__openIndices(archive) - self.threadIndex[key]=msgid - - def getArticle(self, archive, msgid): - self.__openIndices(archive) - if not self.__cache.has_key(msgid): - # get the pickled object out of the DumbBTree - buf = self.articleIndex[msgid] - article = self.__cache[msgid] = pickle.loads(buf) - # For upgrading older archives - article.setListIfUnset(self._mlist) - else: - article = self.__cache[msgid] - return article - - def first(self, archive, index): - self.__openIndices(archive) - index = getattr(self, index + 'Index') - try: - key, msgid = index.first() - return msgid - except KeyError: - return None - - def next(self, archive, index): - self.__openIndices(archive) - index = getattr(self, index + 'Index') - try: - key, msgid = index.next() - return msgid - except KeyError: - return None - - def getOldestArticle(self, archive, subject): - self.__openIndices(archive) - subject = subject.lower() - try: - key, tempid=self.subjectIndex.set_location(subject) - self.subjectIndex.next() - [subject2, date]= key.split('\0') - if subject!=subject2: return None - return tempid - except KeyError: - return None - - def newArchive(self, archive): - pass - - def clearIndex(self, archive, index): - self.__openIndices(archive) - if hasattr(self.threadIndex, 'clear'): - self.threadIndex.clear() - return - finished=0 - try: - key, msgid=self.threadIndex.first() - except KeyError: finished=1 - while not finished: - del self.threadIndex[key] - try: - key, msgid=self.threadIndex.next() - except KeyError: finished=1 diff --git a/mailman/Archiver/__init__.py b/mailman/Archiver/__init__.py deleted file mode 100644 index 322010acb..000000000 --- a/mailman/Archiver/__init__.py +++ /dev/null @@ -1,18 +0,0 @@ -# Copyright (C) 1998-2009 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 . - -from Archiver import * diff --git a/mailman/Archiver/pipermail.py b/mailman/Archiver/pipermail.py deleted file mode 100644 index 19bc05c3f..000000000 --- a/mailman/Archiver/pipermail.py +++ /dev/null @@ -1,874 +0,0 @@ -#! /usr/bin/env python - -import os -import re -import sys -import time -import logging -import mailbox - -import cPickle as pickle - -from cStringIO import StringIO -from email.Utils import parseaddr, parsedate_tz, mktime_tz, formatdate -from string import lowercase - -__version__ = '0.11 (Mailman edition)' -VERSION = __version__ -CACHESIZE = 100 # Number of slots in the cache - -from mailman.Mailbox import ArchiverMailbox -from mailman.core import errors -from mailman.i18n import _ - -SPACE = ' ' - -log = logging.getLogger('mailman.error') - - - -msgid_pat = re.compile(r'(<.*>)') -def strip_separators(s): - "Remove quotes or parenthesization from a Message-ID string" - if not s: - return "" - if s[0] in '"<([' and s[-1] in '">)]': - s = s[1:-1] - return s - -smallNameParts = ['van', 'von', 'der', 'de'] - -def fixAuthor(author): - "Canonicalize a name into Last, First format" - # If there's a comma, guess that it's already in "Last, First" format - if ',' in author: - return author - L = author.split() - i = len(L) - 1 - if i == 0: - return author # The string's one word--forget it - if author.upper() == author or author.lower() == author: - # Damn, the name is all upper- or lower-case. - while i > 0 and L[i-1].lower() in smallNameParts: - i = i - 1 - else: - # Mixed case; assume that small parts of the last name will be - # in lowercase, and check them against the list. - while i>0 and (L[i-1][0] in lowercase or - L[i-1].lower() in smallNameParts): - i = i - 1 - author = SPACE.join(L[-1:] + L[i:-1]) + ', ' + SPACE.join(L[:i]) - return author - -# Abstract class for databases - -class DatabaseInterface: - def __init__(self): pass - def close(self): pass - def getArticle(self, archive, msgid): pass - def hasArticle(self, archive, msgid): pass - def addArticle(self, archive, article, subject=None, author=None, - date=None): pass - def firstdate(self, archive): pass - def lastdate(self, archive): pass - def first(self, archive, index): pass - def next(self, archive, index): pass - def numArticles(self, archive): pass - def newArchive(self, archive): pass - def setThreadKey(self, archive, key, msgid): pass - def getOldestArticle(self, subject): pass - -class Database(DatabaseInterface): - """Define the basic sorting logic for a database - - Assumes that the database internally uses dateIndex, authorIndex, - etc. - """ - - # TBD Factor out more of the logic shared between BSDDBDatabase - # and HyperDatabase and place it in this class. - - def __init__(self): - # This method need not be called by subclasses that do their - # own initialization. - self.dateIndex = {} - self.authorIndex = {} - self.subjectIndex = {} - self.articleIndex = {} - self.changed = {} - - def addArticle(self, archive, article, subject=None, author=None, - date=None): - # create the keys; always end w/ msgid which will be unique - authorkey = (author or article.author, article.date, - article.msgid) - subjectkey = (subject or article.subject, article.date, - article.msgid) - datekey = date or article.date, article.msgid - - # Add the new article - self.dateIndex[datekey] = article.msgid - self.authorIndex[authorkey] = article.msgid - self.subjectIndex[subjectkey] = article.msgid - - self.store_article(article) - self.changed[archive, article.msgid] = None - - parentID = article.parentID - if parentID is not None and self.articleIndex.has_key(parentID): - parent = self.getArticle(archive, parentID) - myThreadKey = parent.threadKey + article.date + '-' - else: - myThreadKey = article.date + '-' - article.threadKey = myThreadKey - key = myThreadKey, article.msgid - self.setThreadKey(archive, key, article.msgid) - - def store_article(self, article): - """Store article without message body to save space""" - # TBD this is not thread safe! - temp = article.body - temp2 = article.html_body - article.body = [] - del article.html_body - self.articleIndex[article.msgid] = pickle.dumps(article) - article.body = temp - article.html_body = temp2 - - -# The Article class encapsulates a single posting. The attributes -# are: -# -# sequence : Sequence number, unique for each article in a set of archives -# subject : Subject -# datestr : The posting date, in human-readable format -# date : The posting date, in purely numeric format -# headers : Any other headers of interest -# author : The author's name (and possibly organization) -# email : The author's e-mail address -# msgid : A unique message ID -# in_reply_to: If != "", this is the msgid of the article being replied to -# references : A (possibly empty) list of msgid's of earlier articles -# in the thread -# body : A list of strings making up the message body - -class Article: - _last_article_time = time.time() - - def __init__(self, message = None, sequence = 0, keepHeaders = []): - if message is None: - return - self.sequence = sequence - - self.parentID = None - self.threadKey = None - # otherwise the current sequence number is used. - id = strip_separators(message['Message-Id']) - if id == "": - self.msgid = str(self.sequence) - else: self.msgid = id - - if message.has_key('Subject'): - self.subject = str(message['Subject']) - else: - self.subject = _('No subject') - if self.subject == "": self.subject = _('No subject') - - self._set_date(message) - - # Figure out the e-mail address and poster's name. Use the From: - # field first, followed by Reply-To: - self.author, self.email = parseaddr(message.get('From', '')) - e = message['Reply-To'] - if not self.email and e is not None: - ignoreauthor, self.email = parseaddr(e) - self.email = strip_separators(self.email) - self.author = strip_separators(self.author) - - if self.author == "": - self.author = self.email - - # Save the In-Reply-To:, References:, and Message-ID: lines - # - # TBD: The original code does some munging on these fields, which - # shouldn't be necessary, but changing this may break code. For - # safety, I save the original headers on different attributes for use - # in writing the plain text periodic flat files. - self._in_reply_to = message['in-reply-to'] - self._references = message['references'] - self._message_id = message['message-id'] - - i_r_t = message['In-Reply-To'] - if i_r_t is None: - self.in_reply_to = '' - else: - match = msgid_pat.search(i_r_t) - if match is None: self.in_reply_to = '' - else: self.in_reply_to = strip_separators(match.group(1)) - - references = message['References'] - if references is None: - self.references = [] - else: - self.references = map(strip_separators, references.split()) - - # Save any other interesting headers - self.headers = {} - for i in keepHeaders: - if message.has_key(i): - self.headers[i] = message[i] - - # Read the message body - s = StringIO(message.get_payload(decode=True)\ - or message.as_string().split('\n\n',1)[1]) - self.body = s.readlines() - - def _set_date(self, message): - def floatdate(header): - missing = [] - datestr = message.get(header, missing) - if datestr is missing: - return None - date = parsedate_tz(datestr) - try: - return mktime_tz(date) - except (TypeError, ValueError, OverflowError): - return None - date = floatdate('date') - if date is None: - date = floatdate('x-list-received-date') - if date is None: - # What's left to try? - date = self._last_article_time + 1 - self._last_article_time = date - self.date = '%011i' % date - self.datestr = message.get('date') \ - or message.get('x-list-received-date') \ - or formatdate(date) - - def __repr__(self): - return '

      ' - - def finished_update_article(self): - pass - -# Pipermail formatter class - -class T: - DIRMODE = 0755 # Mode to give to created directories - FILEMODE = 0644 # Mode to give to created files - INDEX_EXT = ".html" # Extension for indexes - - def __init__(self, basedir = None, reload = 1, database = None): - # If basedir isn't provided, assume the current directory - if basedir is None: - self.basedir = os.getcwd() - else: - basedir = os.path.expanduser(basedir) - self.basedir = basedir - self.database = database - - # If the directory doesn't exist, create it. This code shouldn't get - # run anymore, we create the directory in Archiver.py. It should only - # get used by legacy lists created that are only receiving their first - # message in the HTML archive now -- Marc - try: - os.stat(self.basedir) - except os.error, errdata: - errno, errmsg = errdata - if errno != 2: - raise os.error, errdata - else: - self.message(_('Creating archive directory ') + self.basedir) - omask = os.umask(0) - try: - os.mkdir(self.basedir, self.DIRMODE) - finally: - os.umask(omask) - - # Try to load previously pickled state - try: - if not reload: - raise IOError - f = open(os.path.join(self.basedir, 'pipermail.pck'), 'r') - self.message(_('Reloading pickled archive state')) - d = pickle.load(f) - f.close() - for key, value in d.items(): - setattr(self, key, value) - except (IOError, EOFError): - # No pickled version, so initialize various attributes - self.archives = [] # Archives - self._dirty_archives = [] # Archives that will have to be updated - self.sequence = 0 # Sequence variable used for - # numbering articles - self.update_TOC = 0 # Does the TOC need updating? - # - # make the basedir variable work when passed in as an __init__ arg - # and different from the one in the pickle. Let the one passed in - # as an __init__ arg take precedence if it's stated. This way, an - # archive can be moved from one place to another and still work. - # - if basedir != self.basedir: - self.basedir = basedir - - def close(self): - "Close an archive, save its state, and update any changed archives." - self.update_dirty_archives() - self.update_TOC = 0 - self.write_TOC() - # Save the collective state - self.message(_('Pickling archive state into ') - + os.path.join(self.basedir, 'pipermail.pck')) - self.database.close() - del self.database - - omask = os.umask(007) - try: - f = open(os.path.join(self.basedir, 'pipermail.pck'), 'w') - finally: - os.umask(omask) - pickle.dump(self.getstate(), f) - f.close() - - def getstate(self): - # can override this in subclass - return self.__dict__ - - # - # Private methods - # - # These will be neither overridden nor called by custom archivers. - # - - - # Create a dictionary of various parameters that will be passed - # to the write_index_{header,footer} functions - def __set_parameters(self, archive): - # Determine the earliest and latest date in the archive - firstdate = self.database.firstdate(archive) - lastdate = self.database.lastdate(archive) - - # Get the current time - now = time.asctime(time.localtime(time.time())) - self.firstdate = firstdate - self.lastdate = lastdate - self.archivedate = now - self.size = self.database.numArticles(archive) - self.archive = archive - self.version = __version__ - - # Find the message ID of an article's parent, or return None - # if no parent can be found. - - def __findParent(self, article, children = []): - parentID = None - if article.in_reply_to: - parentID = article.in_reply_to - elif article.references: - # Remove article IDs that aren't in the archive - refs = filter(self.articleIndex.has_key, article.references) - if not refs: - return None - maxdate = self.database.getArticle(self.archive, - refs[0]) - for ref in refs[1:]: - a = self.database.getArticle(self.archive, ref) - if a.date > maxdate.date: - maxdate = a - parentID = maxdate.msgid - else: - # Look for the oldest matching subject - try: - key, tempid = \ - self.subjectIndex.set_location(article.subject) - print key, tempid - self.subjectIndex.next() - [subject, date] = key.split('\0') - print article.subject, subject, date - if subject == article.subject and tempid not in children: - parentID = tempid - except KeyError: - pass - return parentID - - # Update the threaded index completely - def updateThreadedIndex(self): - # Erase the threaded index - self.database.clearIndex(self.archive, 'thread') - - # Loop over all the articles - msgid = self.database.first(self.archive, 'date') - while msgid is not None: - try: - article = self.database.getArticle(self.archive, msgid) - except KeyError: - pass - else: - if article.parentID is None or \ - not self.database.hasArticle(self.archive, - article.parentID): - # then - pass - else: - parent = self.database.getArticle(self.archive, - article.parentID) - article.threadKey = parent.threadKey+article.date+'-' - self.database.setThreadKey(self.archive, - (article.threadKey, article.msgid), - msgid) - msgid = self.database.next(self.archive, 'date') - - # - # Public methods: - # - # These are part of the public interface of the T class, but will - # never be overridden (unless you're trying to do something very new). - - # Update a single archive's indices, whether the archive's been - # dirtied or not. - def update_archive(self, archive): - self.archive = archive - self.message(_("Updating index files for archive [%(archive)s]")) - arcdir = os.path.join(self.basedir, archive) - self.__set_parameters(archive) - - for hdr in ('Date', 'Subject', 'Author'): - self._update_simple_index(hdr, archive, arcdir) - - self._update_thread_index(archive, arcdir) - - def _update_simple_index(self, hdr, archive, arcdir): - self.message(" " + hdr) - self.type = hdr - hdr = hdr.lower() - - self._open_index_file_as_stdout(arcdir, hdr) - self.write_index_header() - count = 0 - # Loop over the index entries - msgid = self.database.first(archive, hdr) - while msgid is not None: - try: - article = self.database.getArticle(self.archive, msgid) - except KeyError: - pass - else: - count = count + 1 - self.write_index_entry(article) - msgid = self.database.next(archive, hdr) - # Finish up this index - self.write_index_footer() - self._restore_stdout() - - def _update_thread_index(self, archive, arcdir): - self.message(_(" Thread")) - self._open_index_file_as_stdout(arcdir, "thread") - self.type = 'Thread' - self.write_index_header() - - # To handle the prev./next in thread pointers, we need to - # track articles 5 at a time. - - # Get the first 5 articles - L = [None] * 5 - i = 2 - msgid = self.database.first(self.archive, 'thread') - - while msgid is not None and i < 5: - L[i] = self.database.getArticle(self.archive, msgid) - i = i + 1 - msgid = self.database.next(self.archive, 'thread') - - while L[2] is not None: - article = L[2] - artkey = None - if article is not None: - artkey = article.threadKey - if artkey is not None: - self.write_threadindex_entry(article, artkey.count('-') - 1) - if self.database.changed.has_key((archive,article.msgid)): - a1 = L[1] - a3 = L[3] - self.update_article(arcdir, article, a1, a3) - if a3 is not None: - self.database.changed[(archive, a3.msgid)] = None - if a1 is not None: - key = archive, a1.msgid - if not self.database.changed.has_key(key): - self.update_article(arcdir, a1, L[0], L[2]) - else: - del self.database.changed[key] - if L[0]: - L[0].finished_update_article() - L = L[1:] # Rotate the list - if msgid is None: - L.append(msgid) - else: - L.append(self.database.getArticle(self.archive, msgid)) - msgid = self.database.next(self.archive, 'thread') - - self.write_index_footer() - self._restore_stdout() - - def _open_index_file_as_stdout(self, arcdir, index_name): - path = os.path.join(arcdir, index_name + self.INDEX_EXT) - omask = os.umask(002) - try: - self.__f = open(path, 'w') - finally: - os.umask(omask) - self.__stdout = sys.stdout - sys.stdout = self.__f - - def _restore_stdout(self): - sys.stdout = self.__stdout - self.__f.close() - del self.__f - del self.__stdout - - # Update only archives that have been marked as "changed". - def update_dirty_archives(self): - for i in self._dirty_archives: - self.update_archive(i) - self._dirty_archives = [] - - # Read a Unix mailbox file from the file object , - # and create a series of Article objects. Each article - # object will then be archived. - - def _makeArticle(self, msg, sequence): - return Article(msg, sequence) - - def processUnixMailbox(self, input, start=None, end=None): - mbox = ArchiverMailbox(input, self.maillist) - if start is None: - start = 0 - counter = 0 - while counter < start: - try: - m = mbox.next() - except errors.DiscardMessage: - continue - if m is None: - return - counter += 1 - while 1: - try: - pos = input.tell() - m = mbox.next() - except errors.DiscardMessage: - continue - except Exception: - log.error('uncaught archiver exception at filepos: %s', pos) - raise - if m is None: - break - if m == '': - # It was an unparseable message - continue - msgid = m.get('message-id', 'n/a') - self.message(_('#%(counter)05d %(msgid)s')) - a = self._makeArticle(m, self.sequence) - self.sequence += 1 - self.add_article(a) - if end is not None and counter >= end: - break - counter += 1 - - def new_archive(self, archive, archivedir): - self.archives.append(archive) - self.update_TOC = 1 - self.database.newArchive(archive) - # If the archive directory doesn't exist, create it - try: - os.stat(archivedir) - except os.error, errdata: - errno, errmsg = errdata - if errno == 2: - omask = os.umask(0) - try: - os.mkdir(archivedir, self.DIRMODE) - finally: - os.umask(omask) - else: - raise os.error, errdata - self.open_new_archive(archive, archivedir) - - def add_article(self, article): - archives = self.get_archives(article) - if not archives: - return - if type(archives) == type(''): - archives = [archives] - - article.filename = filename = self.get_filename(article) - temp = self.format_article(article) - for arch in archives: - self.archive = arch # why do this??? - archivedir = os.path.join(self.basedir, arch) - if arch not in self.archives: - self.new_archive(arch, archivedir) - - # Write the HTML-ized article - self.write_article(arch, temp, os.path.join(archivedir, - filename)) - - if article.decoded.has_key('author'): - author = fixAuthor(article.decoded['author']) - else: - author = fixAuthor(article.author) - if article.decoded.has_key('stripped'): - subject = article.decoded['stripped'].lower() - else: - subject = article.subject.lower() - - article.parentID = parentID = self.get_parent_info(arch, article) - if parentID: - parent = self.database.getArticle(arch, parentID) - article.threadKey = parent.threadKey + article.date + '-' - else: - article.threadKey = article.date + '-' - key = article.threadKey, article.msgid - - self.database.setThreadKey(arch, key, article.msgid) - self.database.addArticle(arch, temp, author=author, - subject=subject) - - if arch not in self._dirty_archives: - self._dirty_archives.append(arch) - - def get_parent_info(self, archive, article): - parentID = None - if article.in_reply_to: - parentID = article.in_reply_to - elif article.references: - refs = self._remove_external_references(article.references) - if refs: - maxdate = self.database.getArticle(archive, refs[0]) - for ref in refs[1:]: - a = self.database.getArticle(archive, ref) - if a.date > maxdate.date: - maxdate = a - parentID = maxdate.msgid - else: - # Get the oldest article with a matching subject, and - # assume this is a follow-up to that article - parentID = self.database.getOldestArticle(archive, - article.subject) - - if parentID and not self.database.hasArticle(archive, parentID): - parentID = None - return parentID - - def write_article(self, index, article, path): - omask = os.umask(002) - try: - f = open(path, 'w') - finally: - os.umask(omask) - temp_stdout, sys.stdout = sys.stdout, f - self.write_article_header(article) - sys.stdout.writelines(article.body) - self.write_article_footer(article) - sys.stdout = temp_stdout - f.close() - - def _remove_external_references(self, refs): - keep = [] - for ref in refs: - if self.database.hasArticle(self.archive, ref): - keep.append(ref) - return keep - - # Abstract methods: these will need to be overridden by subclasses - # before anything useful can be done. - - def get_filename(self, article): - pass - def get_archives(self, article): - """Return a list of indexes where the article should be filed. - A string can be returned if the list only contains one entry, - and the empty list is legal.""" - pass - def format_article(self, article): - pass - def write_index_header(self): - pass - def write_index_footer(self): - pass - def write_index_entry(self, article): - pass - def write_threadindex_entry(self, article, depth): - pass - def write_article_header(self, article): - pass - def write_article_footer(self, article): - pass - def write_article_entry(self, article): - pass - def update_article(self, archivedir, article, prev, next): - pass - def write_TOC(self): - pass - def open_new_archive(self, archive, dir): - pass - def message(self, msg): - pass - - -class BSDDBdatabase(Database): - __super_addArticle = Database.addArticle - - def __init__(self, basedir): - self.__cachekeys = [] - self.__cachedict = {} - self.__currentOpenArchive = None # The currently open indices - self.basedir = os.path.expanduser(basedir) - self.changed = {} # Recently added articles, indexed only by - # message ID - - def firstdate(self, archive): - self.__openIndices(archive) - date = 'None' - try: - date, msgid = self.dateIndex.first() - date = time.asctime(time.localtime(float(date))) - except KeyError: - pass - return date - - def lastdate(self, archive): - self.__openIndices(archive) - date = 'None' - try: - date, msgid = self.dateIndex.last() - date = time.asctime(time.localtime(float(date))) - except KeyError: - pass - return date - - def numArticles(self, archive): - self.__openIndices(archive) - return len(self.dateIndex) - - def addArticle(self, archive, article, subject=None, author=None, - date=None): - self.__openIndices(archive) - self.__super_addArticle(archive, article, subject, author, date) - - # Open the BSDDB files that are being used as indices - # (dateIndex, authorIndex, subjectIndex, articleIndex) - def __openIndices(self, archive): - if self.__currentOpenArchive == archive: - return - - import bsddb - self.__closeIndices() - arcdir = os.path.join(self.basedir, 'database') - omask = os.umask(0) - try: - try: - os.mkdir(arcdir, 02775) - except OSError: - # BAW: Hmm... - pass - finally: - os.umask(omask) - for hdr in ('date', 'author', 'subject', 'article', 'thread'): - path = os.path.join(arcdir, archive + '-' + hdr) - t = bsddb.btopen(path, 'c') - setattr(self, hdr + 'Index', t) - self.__currentOpenArchive = archive - - # Close the BSDDB files that are being used as indices (if they're - # open--this is safe to call if they're already closed) - def __closeIndices(self): - if self.__currentOpenArchive is not None: - pass - for hdr in ('date', 'author', 'subject', 'thread', 'article'): - attr = hdr + 'Index' - if hasattr(self, attr): - index = getattr(self, attr) - if hdr == 'article': - if not hasattr(self, 'archive_length'): - self.archive_length = {} - self.archive_length[self.__currentOpenArchive] = len(index) - index.close() - delattr(self,attr) - self.__currentOpenArchive = None - - def close(self): - self.__closeIndices() - def hasArticle(self, archive, msgid): - self.__openIndices(archive) - return self.articleIndex.has_key(msgid) - def setThreadKey(self, archive, key, msgid): - self.__openIndices(archive) - self.threadIndex[key] = msgid - def getArticle(self, archive, msgid): - self.__openIndices(archive) - if self.__cachedict.has_key(msgid): - self.__cachekeys.remove(msgid) - self.__cachekeys.append(msgid) - return self.__cachedict[msgid] - if len(self.__cachekeys) == CACHESIZE: - delkey, self.__cachekeys = (self.__cachekeys[0], - self.__cachekeys[1:]) - del self.__cachedict[delkey] - s = self.articleIndex[msgid] - article = pickle.loads(s) - self.__cachekeys.append(msgid) - self.__cachedict[msgid] = article - return article - - def first(self, archive, index): - self.__openIndices(archive) - index = getattr(self, index+'Index') - try: - key, msgid = index.first() - return msgid - except KeyError: - return None - def next(self, archive, index): - self.__openIndices(archive) - index = getattr(self, index+'Index') - try: - key, msgid = index.next() - except KeyError: - return None - else: - return msgid - - def getOldestArticle(self, archive, subject): - self.__openIndices(archive) - subject = subject.lower() - try: - key, tempid = self.subjectIndex.set_location(subject) - self.subjectIndex.next() - [subject2, date] = key.split('\0') - if subject != subject2: - return None - return tempid - except KeyError: # XXX what line raises the KeyError? - return None - - def newArchive(self, archive): - pass - - def clearIndex(self, archive, index): - self.__openIndices(archive) - index = getattr(self, index+'Index') - finished = 0 - try: - key, msgid = self.threadIndex.first() - except KeyError: - finished = 1 - while not finished: - del self.threadIndex[key] - try: - key, msgid = self.threadIndex.next() - except KeyError: - finished = 1 - - diff --git a/mailman/Bouncers/BouncerAPI.py b/mailman/Bouncers/BouncerAPI.py deleted file mode 100644 index f4712ec20..000000000 --- a/mailman/Bouncers/BouncerAPI.py +++ /dev/null @@ -1,64 +0,0 @@ -# Copyright (C) 1998-2009 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 . - -"""Contains all the common functionality for msg bounce scanning API. - -This module can also be used as the basis for a bounce detection testing -framework. When run as a script, it expects two arguments, the listname and -the filename containing the bounce message. -""" - -import sys - -# If a bounce detector returns Stop, that means to just discard the message. -# An example is warning messages for temporary delivery problems. These -# shouldn't trigger a bounce notification, but we also don't want to send them -# on to the list administrator. -Stop = object() - - -BOUNCE_PIPELINE = [ - 'DSN', - 'Qmail', - 'Postfix', - 'Yahoo', - 'Caiwireless', - 'Exchange', - 'Exim', - 'Netscape', - 'Compuserve', - 'Microsoft', - 'GroupWise', - 'SMTP32', - 'SimpleMatch', - 'SimpleWarning', - 'Yale', - 'LLNL', - ] - - - -# msg must be a mimetools.Message -def ScanMessages(mlist, msg): - for module in BOUNCE_PIPELINE: - modname = 'mailman.Bouncers.' + module - __import__(modname) - addrs = sys.modules[modname].process(msg) - if addrs: - # Return addrs even if it is Stop. BounceRunner needs this info. - return addrs - return [] diff --git a/mailman/Bouncers/Caiwireless.py b/mailman/Bouncers/Caiwireless.py deleted file mode 100644 index 3bf03cc62..000000000 --- a/mailman/Bouncers/Caiwireless.py +++ /dev/null @@ -1,45 +0,0 @@ -# Copyright (C) 1998-2009 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 . - -"""Parse mystery style generated by MTA at caiwireless.net.""" - -import re -import email - -tcre = re.compile(r'the following recipients did not receive this message:', - re.IGNORECASE) -acre = re.compile(r'<(?P[^>]*)>') - - - -def process(msg): - if msg.get_content_type() <> 'multipart/mixed': - return None - # simple state machine - # 0 == nothing seen - # 1 == tag line seen - state = 0 - # This format thinks it's a MIME, but it really isn't - for line in email.Iterators.body_line_iterator(msg): - line = line.strip() - if state == 0 and tcre.match(line): - state = 1 - elif state == 1 and line: - mo = acre.match(line) - if not mo: - return None - return [mo.group('addr')] diff --git a/mailman/Bouncers/Compuserve.py b/mailman/Bouncers/Compuserve.py deleted file mode 100644 index 2297a72a9..000000000 --- a/mailman/Bouncers/Compuserve.py +++ /dev/null @@ -1,46 +0,0 @@ -# Copyright (C) 1998-2009 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 . - -"""Compuserve has its own weird format for bounces.""" - -import re -import email - -dcre = re.compile(r'your message could not be delivered', re.IGNORECASE) -acre = re.compile(r'Invalid receiver address: (?P.*)') - - - -def process(msg): - # simple state machine - # 0 = nothing seen yet - # 1 = intro line seen - state = 0 - addrs = [] - for line in email.Iterators.body_line_iterator(msg): - if state == 0: - mo = dcre.search(line) - if mo: - state = 1 - elif state == 1: - mo = dcre.search(line) - if mo: - break - mo = acre.search(line) - if mo: - addrs.append(mo.group('addr')) - return addrs diff --git a/mailman/Bouncers/DSN.py b/mailman/Bouncers/DSN.py deleted file mode 100644 index 37e5bcb83..000000000 --- a/mailman/Bouncers/DSN.py +++ /dev/null @@ -1,99 +0,0 @@ -# Copyright (C) 1998-2009 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 . - -"""Parse RFC 3464 (i.e. DSN) bounce formats. - -RFC 3464 obsoletes 1894 which was the old DSN standard. This module has not -been audited for differences between the two. -""" - -from email.Iterators import typed_subpart_iterator -from email.Utils import parseaddr - -from mailman.Bouncers.BouncerAPI import Stop - - - -def check(msg): - # Iterate over each message/delivery-status subpart - addrs = [] - for part in typed_subpart_iterator(msg, 'message', 'delivery-status'): - if not part.is_multipart(): - # Huh? - continue - # Each message/delivery-status contains a list of Message objects - # which are the header blocks. Iterate over those too. - for msgblock in part.get_payload(): - # We try to dig out the Original-Recipient (which is optional) and - # Final-Recipient (which is mandatory, but may not exactly match - # an address on our list). Some MTA's also use X-Actual-Recipient - # as a synonym for Original-Recipient, but some apparently use - # that for other purposes :( - # - # Also grok out Action so we can do something with that too. - action = msgblock.get('action', '').lower() - # Some MTAs have been observed that put comments on the action. - if action.startswith('delayed'): - return Stop - if not action.startswith('fail'): - # Some non-permanent failure, so ignore this block - continue - params = [] - foundp = False - for header in ('original-recipient', 'final-recipient'): - for k, v in msgblock.get_params([], header): - if k.lower() == 'rfc822': - foundp = True - else: - params.append(k) - if foundp: - # Note that params should already be unquoted. - addrs.extend(params) - break - else: - # MAS: This is a kludge, but SMTP-GATEWAY01.intra.home.dk - # has a final-recipient with an angle-addr and no - # address-type parameter at all. Non-compliant, but ... - for param in params: - if param.startswith('<') and param.endswith('>'): - addrs.append(param[1:-1]) - # Uniquify - rtnaddrs = {} - for a in addrs: - if a is not None: - realname, a = parseaddr(a) - rtnaddrs[a] = True - return rtnaddrs.keys() - - - -def process(msg): - # A DSN has been seen wrapped with a "legal disclaimer" by an outgoing MTA - # in a multipart/mixed outer part. - if msg.is_multipart() and msg.get_content_subtype() == 'mixed': - msg = msg.get_payload()[0] - # The above will suffice if the original message 'parts' were wrapped with - # the disclaimer added, but the original DSN can be wrapped as a - # message/rfc822 part. We need to test that too. - if msg.is_multipart() and msg.get_content_type() == 'message/rfc822': - msg = msg.get_payload()[0] - # The report-type parameter should be "delivery-status", but it seems that - # some DSN generating MTAs don't include this on the Content-Type: header, - # so let's relax the test a bit. - if not msg.is_multipart() or msg.get_content_subtype() <> 'report': - return None - return check(msg) diff --git a/mailman/Bouncers/Exchange.py b/mailman/Bouncers/Exchange.py deleted file mode 100644 index cf8beefce..000000000 --- a/mailman/Bouncers/Exchange.py +++ /dev/null @@ -1,48 +0,0 @@ -# Copyright (C) 2002-2009 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 . - -"""Recognizes (some) Microsoft Exchange formats.""" - -import re -import email.Iterators - -scre = re.compile('did not reach the following recipient') -ecre = re.compile('MSEXCH:') -a1cre = re.compile('SMTP=(?P[^;]+); on ') -a2cre = re.compile('(?P[^ ]+) on ') - - - -def process(msg): - addrs = {} - it = email.Iterators.body_line_iterator(msg) - # Find the start line - for line in it: - if scre.search(line): - break - else: - return [] - # Search each line until we hit the end line - for line in it: - if ecre.search(line): - break - mo = a1cre.search(line) - if not mo: - mo = a2cre.search(line) - if mo: - addrs[mo.group('addr')] = 1 - return addrs.keys() diff --git a/mailman/Bouncers/Exim.py b/mailman/Bouncers/Exim.py deleted file mode 100644 index 0f4e7f4cf..000000000 --- a/mailman/Bouncers/Exim.py +++ /dev/null @@ -1,31 +0,0 @@ -# Copyright (C) 1998-2009 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 . - -"""Parse bounce messages generated by Exim. - -Exim adds an X-Failed-Recipients: header to bounce messages containing -an `addresslist' of failed addresses. - -""" - -from email.Utils import getaddresses - - - -def process(msg): - all = msg.get_all('x-failed-recipients', []) - return [a for n, a in getaddresses(all)] diff --git a/mailman/Bouncers/GroupWise.py b/mailman/Bouncers/GroupWise.py deleted file mode 100644 index e74291217..000000000 --- a/mailman/Bouncers/GroupWise.py +++ /dev/null @@ -1,71 +0,0 @@ -# Copyright (C) 1998-2009 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 . - -"""This appears to be the format for Novell GroupWise and NTMail - -X-Mailer: Novell GroupWise Internet Agent 5.5.3.1 -X-Mailer: NTMail v4.30.0012 -X-Mailer: Internet Mail Service (5.5.2653.19) -""" - -import re -from email.Message import Message -from cStringIO import StringIO - -acre = re.compile(r'<(?P[^>]*)>') - - - -def find_textplain(msg): - if msg.get_content_type() == 'text/plain': - return msg - if msg.is_multipart: - for part in msg.get_payload(): - if not isinstance(part, Message): - continue - ret = find_textplain(part) - if ret: - return ret - return None - - - -def process(msg): - if msg.get_content_type() <> 'multipart/mixed' or not msg['x-mailer']: - return None - addrs = {} - # find the first text/plain part in the message - textplain = find_textplain(msg) - if not textplain: - return None - body = StringIO(textplain.get_payload()) - while 1: - line = body.readline() - if not line: - break - mo = acre.search(line) - if mo: - addrs[mo.group('addr')] = 1 - elif '@' in line: - i = line.find(' ') - if i == 0: - continue - if i < 0: - addrs[line] = 1 - else: - addrs[line[:i]] = 1 - return addrs.keys() diff --git a/mailman/Bouncers/LLNL.py b/mailman/Bouncers/LLNL.py deleted file mode 100644 index cc1a08542..000000000 --- a/mailman/Bouncers/LLNL.py +++ /dev/null @@ -1,32 +0,0 @@ -# Copyright (C) 2001-2009 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 . - -"""LLNL's custom Sendmail bounce message.""" - -import re -import email - -acre = re.compile(r',\s*(?P\S+@[^,]+),', re.IGNORECASE) - - - -def process(msg): - for line in email.Iterators.body_line_iterator(msg): - mo = acre.search(line) - if mo: - return [mo.group('addr')] - return [] diff --git a/mailman/Bouncers/Microsoft.py b/mailman/Bouncers/Microsoft.py deleted file mode 100644 index 98d27d4ee..000000000 --- a/mailman/Bouncers/Microsoft.py +++ /dev/null @@ -1,53 +0,0 @@ -# Copyright (C) 1998-2009 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 . - -"""Microsoft's `SMTPSVC' nears I kin tell.""" - -import re -from cStringIO import StringIO - -scre = re.compile(r'transcript of session follows', re.IGNORECASE) - - - -def process(msg): - if msg.get_content_type() <> 'multipart/mixed': - return None - # Find the first subpart, which has no MIME type - try: - subpart = msg.get_payload(0) - except IndexError: - # The message *looked* like a multipart but wasn't - return None - data = subpart.get_payload() - if isinstance(data, list): - # The message is a multi-multipart, so not a matching bounce - return None - body = StringIO(data) - state = 0 - addrs = [] - while 1: - line = body.readline() - if not line: - break - if state == 0: - if scre.search(line): - state = 1 - if state == 1: - if '@' in line: - addrs.append(line) - return addrs diff --git a/mailman/Bouncers/Netscape.py b/mailman/Bouncers/Netscape.py deleted file mode 100644 index 319329e84..000000000 --- a/mailman/Bouncers/Netscape.py +++ /dev/null @@ -1,89 +0,0 @@ -# Copyright (C) 1998-2009 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 . - -"""Netscape Messaging Server bounce formats. - -I've seen at least one NMS server version 3.6 (envy.gmp.usyd.edu.au) bounce -messages of this format. Bounces come in DSN MIME format, but don't include -any -Recipient: headers. Gotta just parse the text :( - -NMS 4.1 (dfw-smtpin1.email.verio.net) seems even worse, but we'll try to -decipher the format here too. - -""" - -import re -from cStringIO import StringIO - -pcre = re.compile( - r'This Message was undeliverable due to the following reason:', - re.IGNORECASE) - -acre = re.compile( - r'(?Pplease reply to)?.*<(?P[^>]*)>', - re.IGNORECASE) - - - -def flatten(msg, leaves): - # give us all the leaf (non-multipart) subparts - if msg.is_multipart(): - for part in msg.get_payload(): - flatten(part, leaves) - else: - leaves.append(msg) - - - -def process(msg): - # Sigh. Some show NMS 3.6's show - # multipart/report; report-type=delivery-status - # and some show - # multipart/mixed; - if not msg.is_multipart(): - return None - # We're looking for a text/plain subpart occuring before a - # message/delivery-status subpart. - plainmsg = None - leaves = [] - flatten(msg, leaves) - for i, subpart in zip(range(len(leaves)-1), leaves): - if subpart.get_content_type() == 'text/plain': - plainmsg = subpart - break - if not plainmsg: - return None - # Total guesswork, based on captured examples... - body = StringIO(plainmsg.get_payload()) - addrs = [] - while 1: - line = body.readline() - if not line: - break - mo = pcre.search(line) - if mo: - # We found a bounce section, but I have no idea what the official - # format inside here is. :( We'll just search for - # strings. - while 1: - line = body.readline() - if not line: - break - mo = acre.search(line) - if mo and not mo.group('reply'): - addrs.append(mo.group('addr')) - return addrs diff --git a/mailman/Bouncers/Postfix.py b/mailman/Bouncers/Postfix.py deleted file mode 100644 index cfc97a05e..000000000 --- a/mailman/Bouncers/Postfix.py +++ /dev/null @@ -1,86 +0,0 @@ -# Copyright (C) 1998-2009 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 . - -"""Parse bounce messages generated by Postfix. - -This also matches something called `Keftamail' which looks just like Postfix -bounces with the word Postfix scratched out and the word `Keftamail' written -in in crayon. - -It also matches something claiming to be `The BNS Postfix program', and -`SMTP_Gateway'. Everybody's gotta be different, huh? -""" - -import re -from cStringIO import StringIO - - - -def flatten(msg, leaves): - # give us all the leaf (non-multipart) subparts - if msg.is_multipart(): - for part in msg.get_payload(): - flatten(part, leaves) - else: - leaves.append(msg) - - - -# are these heuristics correct or guaranteed? -pcre = re.compile(r'[ \t]*the\s*(bns)?\s*(postfix|keftamail|smtp_gateway)', - re.IGNORECASE) -rcre = re.compile(r'failure reason:$', re.IGNORECASE) -acre = re.compile(r'<(?P[^>]*)>:') - -def findaddr(msg): - addrs = [] - body = StringIO(msg.get_payload()) - # simple state machine - # 0 == nothing found - # 1 == salutation found - state = 0 - while 1: - line = body.readline() - if not line: - break - # preserve leading whitespace - line = line.rstrip() - # yes use match to match at beginning of string - if state == 0 and (pcre.match(line) or rcre.match(line)): - state = 1 - elif state == 1 and line: - mo = acre.search(line) - if mo: - addrs.append(mo.group('addr')) - # probably a continuation line - return addrs - - - -def process(msg): - if msg.get_content_type() not in ('multipart/mixed', 'multipart/report'): - return None - # We're looking for the plain/text subpart with a Content-Description: of - # `notification'. - leaves = [] - flatten(msg, leaves) - for subpart in leaves: - if subpart.get_content_type() == 'text/plain' and \ - subpart.get('content-description', '').lower() == 'notification': - # then... - return findaddr(subpart) - return None diff --git a/mailman/Bouncers/Qmail.py b/mailman/Bouncers/Qmail.py deleted file mode 100644 index 2431da653..000000000 --- a/mailman/Bouncers/Qmail.py +++ /dev/null @@ -1,72 +0,0 @@ -# Copyright (C) 1998-2009 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 . - -"""Parse bounce messages generated by qmail. - -Qmail actually has a standard, called QSBMF (qmail-send bounce message -format), as described in - - http://cr.yp.to/proto/qsbmf.txt - -This module should be conformant. - -""" - -import re -import email.Iterators - -# Other (non-standard?) intros have been observed in the wild. -introtags = [ - 'Hi. This is the', - "We're sorry. There's a problem", - 'Check your send e-mail address.', - 'This is the mail delivery agent at', - 'Unfortunately, your mail was not delivered' - ] -acre = re.compile(r'<(?P[^>]*)>:') - - - -def process(msg): - addrs = [] - # simple state machine - # 0 = nothing seen yet - # 1 = intro paragraph seen - # 2 = recip paragraphs seen - state = 0 - for line in email.Iterators.body_line_iterator(msg): - line = line.strip() - if state == 0: - for introtag in introtags: - if line.startswith(introtag): - state = 1 - break - elif state == 1 and not line: - # Looking for the end of the intro paragraph - state = 2 - elif state == 2: - if line.startswith('-'): - # We're looking at the break paragraph, so we're done - break - # At this point we know we must be looking at a recipient - # paragraph - mo = acre.match(line) - if mo: - addrs.append(mo.group('addr')) - # Otherwise, it must be a continuation line, so just ignore it - # Not looking at anything in particular - return addrs diff --git a/mailman/Bouncers/SMTP32.py b/mailman/Bouncers/SMTP32.py deleted file mode 100644 index a7fff2ed3..000000000 --- a/mailman/Bouncers/SMTP32.py +++ /dev/null @@ -1,60 +0,0 @@ -# Copyright (C) 1998-2009 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 . - -"""Something which claims -X-Mailer: - -What the heck is this thing? Here's a recent host: - -% telnet 207.51.255.218 smtp -Trying 207.51.255.218... -Connected to 207.51.255.218. -Escape character is '^]'. -220 X1 NT-ESMTP Server 208.24.118.205 (IMail 6.00 45595-15) - -""" - -import re -import email - -ecre = re.compile('original message follows', re.IGNORECASE) -acre = re.compile(r''' - ( # several different prefixes - user\ mailbox[^:]*: # have been spotted in the - |delivery\ failed[^:]*: # wild... - |unknown\ user[^:]*: - |undeliverable\ +to - |delivery\ userid[^:]*: - ) - \s* # space separator - (?P[^\s]*) # and finally, the address - ''', re.IGNORECASE | re.VERBOSE) - - - -def process(msg): - mailer = msg.get('x-mailer', '') - if not mailer.startswith('. - -"""Recognizes simple heuristically delimited bounces.""" - -import re -import email.Iterators - - - -def _c(pattern): - return re.compile(pattern, re.IGNORECASE) - -# This is a list of tuples of the form -# -# (start cre, end cre, address cre) -# -# where `cre' means compiled regular expression, start is the line just before -# the bouncing address block, end is the line just after the bouncing address -# block, and address cre is the regexp that will recognize the addresses. It -# must have a group called `addr' which will contain exactly and only the -# address that bounced. -PATTERNS = [ - # sdm.de - (_c('here is your list of failed recipients'), - _c('here is your returned mail'), - _c(r'<(?P[^>]*)>')), - # sz-sb.de, corridor.com, nfg.nl - (_c('the following addresses had'), - _c('transcript of session follows'), - _c(r'<(?P[^>]*)>|\(expanded from: [^>)]*)>?\)')), - # robanal.demon.co.uk - (_c('this message was created automatically by mail delivery software'), - _c('original message follows'), - _c('rcpt to:\s*<(?P[^>]*)>')), - # s1.com (InterScan E-Mail VirusWall NT ???) - (_c('message from interscan e-mail viruswall nt'), - _c('end of message'), - _c('rcpt to:\s*<(?P[^>]*)>')), - # Smail - (_c('failed addresses follow:'), - _c('message text follows:'), - _c(r'\s*(?P\S+@\S+)')), - # newmail.ru - (_c('This is the machine generated message from mail service.'), - _c('--- Below the next line is a copy of the message.'), - _c('<(?P[^>]*)>')), - # turbosport.com runs something called `MDaemon 3.5.2' ??? - (_c('The following addresses did NOT receive a copy of your message:'), - _c('--- Session Transcript ---'), - _c('[>]\s*(?P.*)$')), - # usa.net - (_c('Intended recipient:\s*(?P.*)$'), - _c('--------RETURNED MAIL FOLLOWS--------'), - _c('Intended recipient:\s*(?P.*)$')), - # hotpop.com - (_c('Undeliverable Address:\s*(?P.*)$'), - _c('Original message attached'), - _c('Undeliverable Address:\s*(?P.*)$')), - # Another demon.co.uk format - (_c('This message was created automatically by mail delivery'), - _c('^---- START OF RETURNED MESSAGE ----'), - _c("addressed to '(?P[^']*)'")), - # Prodigy.net full mailbox - (_c("User's mailbox is full:"), - _c('Unable to deliver mail.'), - _c("User's mailbox is full:\s*<(?P[^>]*)>")), - # Microsoft SMTPSVC - (_c('The email below could not be delivered to the following user:'), - _c('Old message:'), - _c('<(?P[^>]*)>')), - # Yahoo on behalf of other domains like sbcglobal.net - (_c('Unable to deliver message to the following address\(es\)\.'), - _c('--- Original message follows\.'), - _c('<(?P[^>]*)>:')), - # googlemail.com - (_c('Delivery to the following recipient failed'), - _c('----- Original message -----'), - _c('^\s*(?P[^\s@]+@[^\s@]+)\s*$')), - # kundenserver.de - (_c('A message that you sent could not be delivered'), - _c('^---'), - _c('<(?P[^>]*)>')), - # another kundenserver.de - (_c('A message that you sent could not be delivered'), - _c('^---'), - _c('^(?P[^\s@]+@[^\s@:]+):')), - # thehartford.com - (_c('Delivery to the following recipients failed'), - # this one may or may not have the original message, but there's nothing - # unique to stop on, so stop on the first line of at least 3 characters - # that doesn't start with 'D' (to not stop immediately) and has no '@'. - _c('^[^D][^@]{2,}$'), - _c('^\s*(?P[^\s@]+@[^\s@]+)\s*$')), - # and another thehartfod.com/hartfordlife.com - (_c('^Your message\s*$'), - _c('^because:'), - _c('^\s*(?P[^\s@]+@[^\s@]+)\s*$')), - # kviv.be (InterScan NT) - (_c('^Unable to deliver message to'), - _c(r'\*+\s+End of message\s+\*+'), - _c('<(?P[^>]*)>')), - # earthlink.net supported domains - (_c('^Sorry, unable to deliver your message to'), - _c('^A copy of the original message'), - _c('\s*(?P[^\s@]+@[^\s@]+)\s+')), - # ademe.fr - (_c('^A message could not be delivered to:'), - _c('^Subject:'), - _c('^\s*(?P[^\s@]+@[^\s@]+)\s*$')), - # andrew.ac.jp - (_c('^Invalid final delivery userid:'), - _c('^Original message follows.'), - _c('\s*(?P[^\s@]+@[^\s@]+)\s*$')), - # E500_SMTP_Mail_Service@lerctr.org - (_c('------ Failed Recipients ------'), - _c('-------- Returned Mail --------'), - _c('<(?P[^>]*)>')), - # cynergycom.net - (_c('A message that you sent could not be delivered'), - _c('^---'), - _c('(?P[^\s@]+@[^\s@)]+)')), - # LSMTP for Windows - (_c('^--> Error description:\s*$'), - _c('^Error-End:'), - _c('^Error-for:\s+(?P[^\s@]+@[^\s@]+)')), - # Qmail with a tri-language intro beginning in spanish - (_c('Your message could not be delivered'), - _c('^-'), - _c('<(?P[^>]*)>:')), - # socgen.com - (_c('Your message could not be delivered to'), - _c('^\s*$'), - _c('(?P[^\s@]+@[^\s@]+)')), - # dadoservice.it - (_c('Your message has encountered delivery problems'), - _c('Your message reads'), - _c('addressed to\s*(?P[^\s@]+@[^\s@)]+)')), - # gomaps.com - (_c('Did not reach the following recipient'), - _c('^\s*$'), - _c('\s(?P[^\s@]+@[^\s@]+)')), - # EYOU MTA SYSTEM - (_c('This is the deliver program at'), - _c('^-'), - _c('^(?P[^\s@]+@[^\s@<>]+)')), - # A non-standard qmail at ieo.it - (_c('this is the email server at'), - _c('^-'), - _c('\s(?P[^\s@]+@[^\s@]+)[\s,]')), - # pla.net.py (MDaemon.PRO ?) - (_c('- no such user here'), - _c('There is no user'), - _c('^(?P[^\s@]+@[^\s@]+)\s')), - # Next one goes here... - ] - - - -def process(msg, patterns=None): - if patterns is None: - patterns = PATTERNS - # simple state machine - # 0 = nothing seen yet - # 1 = intro seen - addrs = {} - # MAS: This is a mess. The outer loop used to be over the message - # so we only looped through the message once. Looping through the - # message for each set of patterns is obviously way more work, but - # if we don't do it, problems arise because scre from the wrong - # pattern set matches first and then acre doesn't match. The - # alternative is to split things into separate modules, but then - # we process the message multiple times anyway. - for scre, ecre, acre in patterns: - state = 0 - for line in email.Iterators.body_line_iterator(msg): - if state == 0: - if scre.search(line): - state = 1 - if state == 1: - mo = acre.search(line) - if mo: - addr = mo.group('addr') - if addr: - addrs[mo.group('addr')] = 1 - elif ecre.search(line): - break - if addrs: - break - return addrs.keys() diff --git a/mailman/Bouncers/SimpleWarning.py b/mailman/Bouncers/SimpleWarning.py deleted file mode 100644 index ab18d2530..000000000 --- a/mailman/Bouncers/SimpleWarning.py +++ /dev/null @@ -1,62 +0,0 @@ -# Copyright (C) 2001-2009 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 . - -"""Recognizes simple heuristically delimited warnings.""" - -from mailman.Bouncers.BouncerAPI import Stop -from mailman.Bouncers.SimpleMatch import _c -from mailman.Bouncers.SimpleMatch import process as _process - - - -# This is a list of tuples of the form -# -# (start cre, end cre, address cre) -# -# where `cre' means compiled regular expression, start is the line just before -# the bouncing address block, end is the line just after the bouncing address -# block, and address cre is the regexp that will recognize the addresses. It -# must have a group called `addr' which will contain exactly and only the -# address that bounced. -patterns = [ - # pop3.pta.lia.net - (_c('The address to which the message has not yet been delivered is'), - _c('No action is required on your part'), - _c(r'\s*(?P\S+@\S+)\s*')), - # This is from MessageSwitch. It is a kludge because the text that - # identifies it as a warning only comes after the address. We can't - # use ecre, because it really isn't significant, so we fake it. Once - # we see the start, we know it's a warning, and we're going to return - # Stop anyway, so we match anything for the address and end. - (_c('This is just a warning, you do not need to take any action'), - _c('.+'), - _c('(?P.+)')), - # Symantec_AntiVirus_for_SMTP_Gateways - see comments for MessageSwitch - (_c('Delivery attempts will continue to be made'), - _c('.+'), - _c('(?P.+)')), - # Next one goes here... - ] - - - -def process(msg): - if _process(msg, patterns): - # It's a recognized warning so stop now - return Stop - else: - return [] diff --git a/mailman/Bouncers/Sina.py b/mailman/Bouncers/Sina.py deleted file mode 100644 index a6b8e0911..000000000 --- a/mailman/Bouncers/Sina.py +++ /dev/null @@ -1,48 +0,0 @@ -# Copyright (C) 2002-2009 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 . - -"""sina.com bounces""" - -import re -from email import Iterators - -acre = re.compile(r'<(?P[^>]*)>') - - - -def process(msg): - if msg.get('from', '').lower() <> 'mailer-daemon@sina.com': - print 'out 1' - return [] - if not msg.is_multipart(): - print 'out 2' - return [] - # The interesting bits are in the first text/plain multipart - part = None - try: - part = msg.get_payload(0) - except IndexError: - pass - if not part: - print 'out 3' - return [] - addrs = {} - for line in Iterators.body_line_iterator(part): - mo = acre.match(line) - if mo: - addrs[mo.group('addr')] = 1 - return addrs.keys() diff --git a/mailman/Bouncers/Yahoo.py b/mailman/Bouncers/Yahoo.py deleted file mode 100644 index b0480b818..000000000 --- a/mailman/Bouncers/Yahoo.py +++ /dev/null @@ -1,54 +0,0 @@ -# Copyright (C) 1998-2009 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 . - -"""Yahoo! has its own weird format for bounces.""" - -import re -import email -from email.Utils import parseaddr - -tcre = re.compile(r'message\s+from\s+yahoo\.\S+', re.IGNORECASE) -acre = re.compile(r'<(?P[^>]*)>:') -ecre = re.compile(r'--- Original message follows') - - - -def process(msg): - # Yahoo! bounces seem to have a known subject value and something called - # an x-uidl: header, the value of which seems unimportant. - sender = parseaddr(msg.get('from', '').lower())[1] or '' - if not sender.startswith('mailer-daemon@yahoo'): - return None - addrs = [] - # simple state machine - # 0 == nothing seen - # 1 == tag line seen - state = 0 - for line in email.Iterators.body_line_iterator(msg): - line = line.strip() - if state == 0 and tcre.match(line): - state = 1 - elif state == 1: - mo = acre.match(line) - if mo: - addrs.append(mo.group('addr')) - continue - mo = ecre.match(line) - if mo: - # we're at the end of the error response - break - return addrs diff --git a/mailman/Bouncers/Yale.py b/mailman/Bouncers/Yale.py deleted file mode 100644 index 956dfb838..000000000 --- a/mailman/Bouncers/Yale.py +++ /dev/null @@ -1,80 +0,0 @@ -# Copyright (C) 2000-2009 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 . - -"""Yale's mail server is pretty dumb. - -Its reports include the end user's name, but not the full domain. I think we -can usually guess it right anyway. This is completely based on examination of -the corpse, and is subject to failure whenever Yale even slightly changes -their MTA. :( - -""" - -import re -from cStringIO import StringIO -from email.Utils import getaddresses - -scre = re.compile(r'Message not delivered to the following', re.IGNORECASE) -ecre = re.compile(r'Error Detail', re.IGNORECASE) -acre = re.compile(r'\s+(?P\S+)\s+') - - - -def process(msg): - if msg.is_multipart(): - return None - try: - whofrom = getaddresses([msg.get('from', '')])[0][1] - if not whofrom: - return None - username, domain = whofrom.split('@', 1) - except (IndexError, ValueError): - return None - if username.lower() <> 'mailer-daemon': - return None - parts = domain.split('.') - parts.reverse() - for part1, part2 in zip(parts, ('edu', 'yale')): - if part1 <> part2: - return None - # Okay, we've established that the bounce came from the mailer-daemon at - # yale.edu. Let's look for a name, and then guess the relevant domains. - names = {} - body = StringIO(msg.get_payload()) - state = 0 - # simple state machine - # 0 == init - # 1 == intro found - while 1: - line = body.readline() - if not line: - break - if state == 0 and scre.search(line): - state = 1 - elif state == 1 and ecre.search(line): - break - elif state == 1: - mo = acre.search(line) - if mo: - names[mo.group('addr')] = 1 - # Now we have a bunch of names, these are either @yale.edu or - # @cs.yale.edu. Add them both. - addrs = [] - for name in names.keys(): - addrs.append(name + '@yale.edu') - addrs.append(name + '@cs.yale.edu') - return addrs diff --git a/mailman/Bouncers/__init__.py b/mailman/Bouncers/__init__.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/mailman/Mailbox.py b/mailman/Mailbox.py deleted file mode 100644 index 3a2f079c4..000000000 --- a/mailman/Mailbox.py +++ /dev/null @@ -1,106 +0,0 @@ -# Copyright (C) 1998-2009 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 . - -"""Extend mailbox.UnixMailbox. -""" - -import sys -import email -import mailbox - -from email.errors import MessageParseError -from email.generator import Generator - -from mailman.Message import Message -from mailman.config import config - - - -def _safeparser(fp): - try: - return email.message_from_file(fp, Message) - except MessageParseError: - # Don't return None since that will stop a mailbox iterator - return '' - - - -class Mailbox(mailbox.PortableUnixMailbox): - def __init__(self, fp): - mailbox.PortableUnixMailbox.__init__(self, fp, _safeparser) - - # msg should be an rfc822 message or a subclass. - def AppendMessage(self, msg): - # Check the last character of the file and write a newline if it isn't - # a newline (but not at the beginning of an empty file). - try: - self.fp.seek(-1, 2) - except IOError, e: - # Assume the file is empty. We can't portably test the error code - # returned, since it differs per platform. - pass - else: - if self.fp.read(1) <> '\n': - self.fp.write('\n') - # Seek to the last char of the mailbox - self.fp.seek(1, 2) - # Create a Generator instance to write the message to the file - g = Generator(self.fp) - g.flatten(msg, unixfrom=True) - # Add one more trailing newline for separation with the next message - # to be appended to the mbox. - print >> self.fp - - - -# This stuff is used by pipermail.py:processUnixMailbox(). It provides an -# opportunity for the built-in archiver to scrub archived messages of nasty -# things like attachments and such... -def _archfactory(mailbox): - # The factory gets a file object, but it also needs to have a MailList - # object, so the clearest way to do this is to build a factory - # function that has a reference to the mailbox object, which in turn holds - # a reference to the mailing list. Nested scopes would help here, BTW, - # but we can't rely on them being around (e.g. Python 2.0). - def scrubber(fp, mailbox=mailbox): - msg = _safeparser(fp) - if msg == '': - return msg - return mailbox.scrub(msg) - return scrubber - - -class ArchiverMailbox(Mailbox): - # This is a derived class which is instantiated with a reference to the - # MailList object. It is build such that the factory calls back into its - # scrub() method, giving the scrubber module a chance to do its thing - # before the message is archived. - def __init__(self, fp, mlist): - scrubber_module = config.scrubber.archive_scrubber - if scrubber_module: - __import__(scrubber_module) - self._scrubber = sys.modules[scrubber_module].process - else: - self._scrubber = None - self._mlist = mlist - mailbox.PortableUnixMailbox.__init__(self, fp, _archfactory(self)) - - def scrub(self, msg): - if self._scrubber: - return self._scrubber(self._mlist, msg) - else: - return msg diff --git a/mailman/Message.py b/mailman/Message.py deleted file mode 100644 index ac41a758c..000000000 --- a/mailman/Message.py +++ /dev/null @@ -1,297 +0,0 @@ -# Copyright (C) 1998-2009 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 . - -"""Standard Mailman message object. - -This is a subclass of email.message.Message but provides a slightly extended -interface which is more convenient for use inside Mailman. -""" - -import re -import email -import email.message -import email.utils - -from email.charset import Charset -from email.header import Header -from lazr.config import as_boolean - -from mailman import Utils -from mailman.config import config - -COMMASPACE = ', ' - -mo = re.match(r'([\d.]+)', email.__version__) -VERSION = tuple(int(s) for s in mo.group().split('.')) - - - -class Message(email.message.Message): - def __init__(self): - # We need a version number so that we can optimize __setstate__() - self.__version__ = VERSION - email.message.Message.__init__(self) - - def __getitem__(self, key): - value = email.message.Message.__getitem__(self, key) - if isinstance(value, str): - return unicode(value, 'ascii') - return value - - def get(self, name, failobj=None): - value = email.message.Message.get(self, name, failobj) - if isinstance(value, str): - return unicode(value, 'ascii') - return value - - def get_all(self, name, failobj=None): - missing = object() - all_values = email.message.Message.get_all(self, name, missing) - if all_values is missing: - return failobj - return [(unicode(value, 'ascii') if isinstance(value, str) else value) - for value in all_values] - - # BAW: For debugging w/ bin/dumpdb. Apparently pprint uses repr. - def __repr__(self): - return self.__str__() - - def __setstate__(self, d): - # The base class attributes have changed over time. Which could - # affect Mailman if messages are sitting in the queue at the time of - # upgrading the email package. We shouldn't burden email with this, - # so we handle schema updates here. - self.__dict__ = d - # We know that email 2.4.3 is up-to-date - version = d.get('__version__', (0, 0, 0)) - d['__version__'] = VERSION - if version >= VERSION: - return - # Messages grew a _charset attribute between email version 0.97 and 1.1 - if not d.has_key('_charset'): - self._charset = None - # Messages grew a _default_type attribute between v2.1 and v2.2 - if not d.has_key('_default_type'): - # We really have no idea whether this message object is contained - # inside a multipart/digest or not, so I think this is the best we - # can do. - self._default_type = 'text/plain' - # Header instances used to allow both strings and Charsets in their - # _chunks, but by email 2.4.3 now it's just Charsets. - headers = [] - hchanged = 0 - for k, v in self._headers: - if isinstance(v, Header): - chunks = [] - cchanged = 0 - for s, charset in v._chunks: - if isinstance(charset, str): - charset = Charset(charset) - cchanged = 1 - chunks.append((s, charset)) - if cchanged: - v._chunks = chunks - hchanged = 1 - headers.append((k, v)) - if hchanged: - self._headers = headers - - # I think this method ought to eventually be deprecated - def get_sender(self): - """Return the address considered to be the author of the email. - - This can return either the From: header, the Sender: header or the - envelope header (a.k.a. the unixfrom header). The first non-empty - header value found is returned. However the search order is - determined by the following: - - - If config.mailman.use_envelope_sender is true, then the search order - is Sender:, From:, unixfrom - - - Otherwise, the search order is From:, Sender:, unixfrom - - unixfrom should never be empty. The return address is always - lower cased. - - This method differs from get_senders() in that it returns one and only - one address, and uses a different search order. - """ - senderfirst = as_boolean(config.mailman.use_envelope_sender) - if senderfirst: - headers = ('sender', 'from') - else: - headers = ('from', 'sender') - for h in headers: - # Use only the first occurrance of Sender: or From:, although it's - # not likely there will be more than one. - fieldval = self[h] - if not fieldval: - continue - addrs = email.utils.getaddresses([fieldval]) - try: - realname, address = addrs[0] - except IndexError: - continue - if address: - break - else: - # We didn't find a non-empty header, so let's fall back to the - # unixfrom address. This should never be empty, but if it ever - # is, it's probably a Really Bad Thing. Further, we just assume - # that if the unixfrom exists, the second field is the address. - unixfrom = self.get_unixfrom() - if unixfrom: - address = unixfrom.split()[1] - else: - # TBD: now what?! - address = '' - return address.lower() - - def get_senders(self): - """Return a list of addresses representing the author of the email. - - The list will contain the following addresses (in order) - depending on availability: - - 1. From: - 2. unixfrom (From_) - 3. Reply-To: - 4. Sender: - - The return addresses are always lower cased. - """ - pairs = [] - for header in config.mailman.sender_headers.split(): - header = header.lower() - if header == 'from_': - # get_unixfrom() returns None if there's no envelope - unix_from = self.get_unixfrom() - fieldval = (unix_from if unix_from is not None else '') - try: - pairs.append(('', fieldval.split()[1])) - except IndexError: - # Ignore badly formatted unixfroms - pass - else: - fieldvals = self.get_all(header) - if fieldvals: - pairs.extend(email.utils.getaddresses(fieldvals)) - authors = [] - for pair in pairs: - address = pair[1] - if address is not None: - address = address.lower() - authors.append(address) - return authors - - def get_filename(self, failobj=None): - """Some MUA have bugs in RFC2231 filename encoding and cause - Mailman to stop delivery in Scrubber.py (called from ToDigest.py). - """ - try: - filename = email.message.Message.get_filename(self, failobj) - return filename - except (UnicodeError, LookupError, ValueError): - return failobj - - - -class UserNotification(Message): - """Class for internally crafted messages.""" - - def __init__(self, recip, sender, subject=None, text=None, lang=None): - Message.__init__(self) - charset = 'us-ascii' - if lang is not None: - charset = Utils.GetCharSet(lang) - if text is not None: - self.set_payload(text.encode(charset), charset) - if subject is None: - subject = '(no subject)' - self['Subject'] = Header(subject.encode(charset), charset, - header_name='Subject', errors='replace') - self['From'] = sender - if isinstance(recip, list): - self['To'] = COMMASPACE.join(recip) - self.recips = recip - else: - self['To'] = recip - self.recips = [recip] - - def send(self, mlist, **_kws): - """Sends the message by enqueuing it to the 'virgin' queue. - - This is used for all internally crafted messages. - """ - # Since we're crafting the message from whole cloth, let's make sure - # this message has a Message-ID. - if 'message-id' not in self: - self['Message-ID'] = email.utils.make_msgid() - # Ditto for Date: as required by RFC 2822. - if 'date' not in self: - self['Date'] = email.utils.formatdate(localtime=True) - # UserNotifications are typically for admin messages, and for messages - # other than list explosions. Send these out as Precedence: bulk, but - # don't override an existing Precedence: header. - if 'precedence' not in self: - self['Precedence'] = 'bulk' - self._enqueue(mlist, **_kws) - - def _enqueue(self, mlist, **_kws): - # Not imported at module scope to avoid import loop - virginq = config.switchboards['virgin'] - # The message metadata better have a 'recip' attribute. - enqueue_kws = dict( - recips=self.recips, - nodecorate=True, - reduced_list_headers=True, - ) - if mlist is not None: - enqueue_kws['listname'] = mlist.fqdn_listname - enqueue_kws.update(_kws) - virginq.enqueue(self, **enqueue_kws) - - - -class OwnerNotification(UserNotification): - """Like user notifications, but this message goes to the list owners.""" - - def __init__(self, mlist, subject=None, text=None, tomoderators=True): - if tomoderators: - roster = mlist.moderators - else: - roster = mlist.owners - recips = [address.address for address in roster.addresses] - sender = config.mailman.site_owner - lang = mlist.preferred_language - UserNotification.__init__(self, recips, sender, subject, text, lang) - # Hack the To header to look like it's going to the -owner address - del self['to'] - self['To'] = mlist.owner_address - self._sender = sender - - def _enqueue(self, mlist, **_kws): - # Not imported at module scope to avoid import loop - virginq = config.switchboards['virgin'] - # The message metadata better have a `recip' attribute - virginq.enqueue(self, - listname=mlist.fqdn_listname, - recips=self.recips, - nodecorate=True, - reduced_list_headers=True, - envsender=self._sender, - **_kws) diff --git a/mailman/Utils.py b/mailman/Utils.py deleted file mode 100644 index 9946273c9..000000000 --- a/mailman/Utils.py +++ /dev/null @@ -1,702 +0,0 @@ -# Copyright (C) 1998-2009 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 . - -"""Miscellaneous essential routines. - -This includes actual message transmission routines, address checking and -message and address munging, a handy-dandy routine to map a function on all -the mailing lists, and whatever else doesn't belong elsewhere. -""" - -import os -import re -import cgi -import time -import errno -import base64 -import random -import logging -import htmlentitydefs -import email.Header -import email.Iterators - -from email.Errors import HeaderParseError -from lazr.config import as_boolean -from string import ascii_letters, digits, whitespace - -import mailman.templates - -from mailman import passwords -from mailman.config import config -from mailman.core import errors -from mailman.utilities.string import expand - - -AT = '@' -CR = '\r' -DOT = '.' -EMPTYSTRING = '' -IDENTCHARS = ascii_letters + digits + '_' -NL = '\n' -UEMPTYSTRING = u'' -TEMPLATE_DIR = os.path.dirname(mailman.templates.__file__) - -# Search for $(identifier)s strings, except that the trailing s is optional, -# since that's a common mistake -cre = re.compile(r'%\(([_a-z]\w*?)\)s?', re.IGNORECASE) -# Search for $$, $identifier, or ${identifier} -dre = re.compile(r'(\${2})|\$([_a-z]\w*)|\${([_a-z]\w*)}', re.IGNORECASE) - -log = logging.getLogger('mailman.error') - - - -# a much more naive implementation than say, Emacs's fill-paragraph! -def wrap(text, column=70, honor_leading_ws=True): - """Wrap and fill the text to the specified column. - - Wrapping is always in effect, although if it is not possible to wrap a - line (because some word is longer than `column' characters) the line is - broken at the next available whitespace boundary. Paragraphs are also - always filled, unless honor_leading_ws is true and the line begins with - whitespace. This is the algorithm that the Python FAQ wizard uses, and - seems like a good compromise. - - """ - wrapped = '' - # first split the text into paragraphs, defined as a blank line - paras = re.split('\n\n', text) - for para in paras: - # fill - lines = [] - fillprev = False - for line in para.split(NL): - if not line: - lines.append(line) - continue - if honor_leading_ws and line[0] in whitespace: - fillthis = False - else: - fillthis = True - if fillprev and fillthis: - # if the previous line should be filled, then just append a - # single space, and the rest of the current line - lines[-1] = lines[-1].rstrip() + ' ' + line - else: - # no fill, i.e. retain newline - lines.append(line) - fillprev = fillthis - # wrap each line - for text in lines: - while text: - if len(text) <= column: - line = text - text = '' - else: - bol = column - # find the last whitespace character - while bol > 0 and text[bol] not in whitespace: - bol -= 1 - # now find the last non-whitespace character - eol = bol - while eol > 0 and text[eol] in whitespace: - eol -= 1 - # watch out for text that's longer than the column width - if eol == 0: - # break on whitespace after column - eol = column - while eol < len(text) and text[eol] not in whitespace: - eol += 1 - bol = eol - while bol < len(text) and text[bol] in whitespace: - bol += 1 - bol -= 1 - line = text[:eol+1] + '\n' - # find the next non-whitespace character - bol += 1 - while bol < len(text) and text[bol] in whitespace: - bol += 1 - text = text[bol:] - wrapped += line - wrapped += '\n' - # end while text - wrapped += '\n' - # end for text in lines - # the last two newlines are bogus - return wrapped[:-2] - - - -def QuotePeriods(text): - JOINER = '\n .\n' - SEP = '\n.\n' - return JOINER.join(text.split(SEP)) - - -# This takes an email address, and returns a tuple containing (user,host) -def ParseEmail(email): - user = None - domain = None - email = email.lower() - at_sign = email.find('@') - if at_sign < 1: - return email, None - user = email[:at_sign] - rest = email[at_sign+1:] - domain = rest.split('.') - return user, domain - - -def LCDomain(addr): - "returns the address with the domain part lowercased" - atind = addr.find('@') - if atind == -1: # no domain part - return addr - return addr[:atind] + '@' + addr[atind+1:].lower() - - -# TBD: what other characters should be disallowed? -_badchars = re.compile(r'[][()<>|;^,\000-\037\177-\377]') - -def ValidateEmail(s): - """Verify that the an email address isn't grossly evil.""" - # Pretty minimal, cheesy check. We could do better... - if not s or ' ' in s: - raise errors.InvalidEmailAddress(repr(s)) - if _badchars.search(s) or s[0] == '-': - raise errors.InvalidEmailAddress(repr(s)) - user, domain_parts = ParseEmail(s) - # Local, unqualified addresses are not allowed. - if not domain_parts: - raise errors.InvalidEmailAddress(repr(s)) - if len(domain_parts) < 2: - raise errors.InvalidEmailAddress(repr(s)) - - - -# Patterns which may be used to form malicious path to inject a new -# line in the mailman error log. (TK: advisory by Moritz Naumann) -CRNLpat = re.compile(r'[^\x21-\x7e]') - -def GetPathPieces(envar='PATH_INFO'): - path = os.environ.get(envar) - if path: - if CRNLpat.search(path): - path = CRNLpat.split(path)[0] - log.error('Warning: Possible malformed path attack.') - return [p for p in path.split('/') if p] - return [] - - - -def ScriptURL(target): - up = '../' * len(GetPathPieces()) - return '%s%s' % (up, target + config.CGIEXT) - - - -def GetPossibleMatchingAddrs(name): - """returns a sorted list of addresses that could possibly match - a given name. - - For Example, given scott@pobox.com, return ['scott@pobox.com'], - given scott@blackbox.pobox.com return ['scott@blackbox.pobox.com', - 'scott@pobox.com']""" - - name = name.lower() - user, domain = ParseEmail(name) - res = [name] - if domain: - domain = domain[1:] - while len(domain) >= 2: - res.append("%s@%s" % (user, DOT.join(domain))) - domain = domain[1:] - return res - - - -def List2Dict(L, foldcase=False): - """Return a dict keyed by the entries in the list passed to it.""" - d = {} - if foldcase: - for i in L: - d[i.lower()] = True - else: - for i in L: - d[i] = True - return d - - - -_vowels = ('a', 'e', 'i', 'o', 'u') -_consonants = ('b', 'c', 'd', 'f', 'g', 'h', 'k', 'm', 'n', - 'p', 'r', 's', 't', 'v', 'w', 'x', 'z') -_syllables = [] - -for v in _vowels: - for c in _consonants: - _syllables.append(c+v) - _syllables.append(v+c) -del c, v - -def UserFriendly_MakeRandomPassword(length): - syls = [] - while len(syls) * 2 < length: - syls.append(random.choice(_syllables)) - return EMPTYSTRING.join(syls)[:length] - - -def Secure_MakeRandomPassword(length): - bytesread = 0 - bytes = [] - fd = None - try: - while bytesread < length: - try: - # Python 2.4 has this on available systems. - newbytes = os.urandom(length - bytesread) - except (AttributeError, NotImplementedError): - if fd is None: - try: - fd = os.open('/dev/urandom', os.O_RDONLY) - except OSError, e: - if e.errno <> errno.ENOENT: - raise - # We have no available source of cryptographically - # secure random characters. Log an error and fallback - # to the user friendly passwords. - log.error( - 'urandom not available, passwords not secure') - return UserFriendly_MakeRandomPassword(length) - newbytes = os.read(fd, length - bytesread) - bytes.append(newbytes) - bytesread += len(newbytes) - s = base64.encodestring(EMPTYSTRING.join(bytes)) - # base64 will expand the string by 4/3rds - return s.replace('\n', '')[:length] - finally: - if fd is not None: - os.close(fd) - - -def MakeRandomPassword(length=None): - if length is None: - length = int(config.passwords.member_password_length) - if as_boolean(config.passwords.user_friendly_passwords): - password = UserFriendly_MakeRandomPassword(length) - else: - password = Secure_MakeRandomPassword(length) - return password.decode('ascii') - - -def GetRandomSeed(): - chr1 = int(random.random() * 52) - chr2 = int(random.random() * 52) - def mkletter(c): - if 0 <= c < 26: - c += 65 - if 26 <= c < 52: - #c = c - 26 + 97 - c += 71 - return c - return "%c%c" % tuple(map(mkletter, (chr1, chr2))) - - - -def set_global_password(pw, siteadmin=True, scheme=None): - if scheme is None: - scheme = passwords.Schemes.ssha - if siteadmin: - filename = config.SITE_PW_FILE - else: - filename = config.LISTCREATOR_PW_FILE - try: - fp = open(filename, 'w') - print >> fp, passwords.make_secret(pw, scheme) - finally: - fp.close() - - -def get_global_password(siteadmin=True): - if siteadmin: - filename = config.SITE_PW_FILE - else: - filename = config.LISTCREATOR_PW_FILE - try: - fp = open(filename) - challenge = fp.read()[:-1] # strip off trailing nl - fp.close() - except IOError, e: - if e.errno <> errno.ENOENT: - raise - # It's okay not to have a site admin password - return None - return challenge - - -def check_global_password(response, siteadmin=True): - challenge = get_global_password(siteadmin) - if challenge is None: - return False - return passwords.check_response(challenge, response) - - - -def websafe(s): - return cgi.escape(s, quote=True) - - -def nntpsplit(s): - parts = s.split(':', 1) - if len(parts) == 2: - try: - return parts[0], int(parts[1]) - except ValueError: - pass - # Use the defaults - return s, 119 - - - -# Just changing these two functions should be enough to control the way -# that email address obscuring is handled. -def ObscureEmail(addr, for_text=False): - """Make email address unrecognizable to web spiders, but invertable. - - When for_text option is set (not default), make a sentence fragment - instead of a token.""" - if for_text: - return addr.replace('@', ' at ') - else: - return addr.replace('@', '--at--') - -def UnobscureEmail(addr): - """Invert ObscureEmail() conversion.""" - # Contrived to act as an identity operation on already-unobscured - # emails, so routines expecting obscured ones will accept both. - return addr.replace('--at--', '@') - - - -class OuterExit(Exception): - pass - -def findtext(templatefile, raw_dict=None, raw=False, lang=None, mlist=None): - # Make some text from a template file. The order of searches depends on - # whether mlist and lang are provided. Once the templatefile is found, - # string substitution is performed by interpolation in `dict'. If `raw' - # is false, the resulting text is wrapped/filled by calling wrap(). - # - # When looking for a template in a specific language, there are 4 places - # that are searched, in this order: - # - # 1. the list-specific language directory - # lists// - # - # 2. the domain-specific language directory - # templates// - # - # 3. the site-wide language directory - # templates/site/ - # - # 4. the global default language directory - # templates/ - # - # The first match found stops the search. In this way, you can specialize - # templates at the desired level, or, if you use only the default - # templates, you don't need to change anything. You should never modify - # files in the templates/ subdirectory, since Mailman will - # overwrite these when you upgrade. That's what the templates/site - # language directories are for. - # - # A further complication is that the language to search for is determined - # by both the `lang' and `mlist' arguments. The search order there is - # that if lang is given, then the 4 locations above are searched, - # substituting lang for . If no match is found, and mlist is - # given, then the 4 locations are searched using the list's preferred - # language. After that, the server default language is used for - # . If that still doesn't yield a template, then the standard - # distribution's English language template is used as an ultimate - # fallback, and when lang is not 'en', the resulting template is passed - # through the translation service. If this template is missing you've got - # big problems. ;) - # - # A word on backwards compatibility: Mailman versions prior to 2.1 stored - # templates in templates/*.{html,txt} and lists//*.{html,txt}. - # Those directories are no longer searched so if you've got customizations - # in those files, you should move them to the appropriate directory based - # on the above description. Mailman's upgrade script cannot do this for - # you. - # - # The function has been revised and renamed as it now returns both the - # template text and the path from which it retrieved the template. The - # original function is now a wrapper which just returns the template text - # as before, by calling this renamed function and discarding the second - # item returned. - # - # Calculate the languages to scan - languages = set() - if lang is not None: - languages.add(lang) - if mlist is not None: - languages.add(mlist.preferred_language) - languages.add(config.mailman.default_language) - assert None not in languages, 'None in languages' - # Calculate the locations to scan - searchdirs = [] - if mlist is not None: - searchdirs.append(mlist.data_path) - searchdirs.append(os.path.join(TEMPLATE_DIR, mlist.host_name)) - searchdirs.append(os.path.join(TEMPLATE_DIR, 'site')) - searchdirs.append(TEMPLATE_DIR) - # Start scanning - fp = None - try: - for lang in languages: - for dir in searchdirs: - filename = os.path.join(dir, lang, templatefile) - try: - fp = open(filename) - raise OuterExit - except IOError, e: - if e.errno <> errno.ENOENT: - raise - # Okay, it doesn't exist, keep looping - fp = None - except OuterExit: - pass - if fp is None: - # Try one last time with the distro English template, which, unless - # you've got a really broken installation, must be there. - try: - filename = os.path.join(TEMPLATE_DIR, 'en', templatefile) - fp = open(filename) - except IOError, e: - if e.errno <> errno.ENOENT: - raise - # We never found the template. BAD! - raise IOError(errno.ENOENT, 'No template file found', templatefile) - else: - from mailman.i18n import get_translation - # XXX BROKEN HACK - data = fp.read()[:-1] - template = get_translation().ugettext(data) - fp.close() - else: - template = fp.read() - fp.close() - template = unicode(template, GetCharSet(lang), 'replace') - text = template - if raw_dict is not None: - text = expand(template, raw_dict) - if raw: - return text, filename - return wrap(text), filename - - -def maketext(templatefile, dict=None, raw=False, lang=None, mlist=None): - return findtext(templatefile, dict, raw, lang, mlist)[0] - - - -def GetRequestURI(fallback=None, escape=True): - """Return the full virtual path this CGI script was invoked with. - - Newer web servers seems to supply this info in the REQUEST_URI - environment variable -- which isn't part of the CGI/1.1 spec. - Thus, if REQUEST_URI isn't available, we concatenate SCRIPT_NAME - and PATH_INFO, both of which are part of CGI/1.1. - - Optional argument `fallback' (default `None') is returned if both of - the above methods fail. - - The url will be cgi escaped to prevent cross-site scripting attacks, - unless `escape' is set to 0. - """ - url = fallback - if 'REQUEST_URI' in os.environ: - url = os.environ['REQUEST_URI'] - elif 'SCRIPT_NAME' in os.environ and 'PATH_INFO' in os.environ: - url = os.environ['SCRIPT_NAME'] + os.environ['PATH_INFO'] - if escape: - return websafe(url) - return url - - - -# XXX Replace this with direct calls. For now, existing uses of GetCharSet() -# are too numerous to change. -def GetCharSet(lang): - return config.languages.get_charset(lang) - - - -def get_request_domain(): - host = os.environ.get('HTTP_HOST', os.environ.get('SERVER_NAME')) - port = os.environ.get('SERVER_PORT') - # Strip off the port if there is one - if port and host.endswith(':' + port): - host = host[:-len(port)-1] - return host.lower() - - -def get_site_noreply(): - return '%s@%s' % (config.NO_REPLY_ADDRESS, config.DEFAULT_EMAIL_HOST) - - - -# Figure out epoch seconds of midnight at the start of today (or the given -# 3-tuple date of (year, month, day). -def midnight(date=None): - if date is None: - date = time.localtime()[:3] - # -1 for dst flag tells the library to figure it out - return time.mktime(date + (0,)*5 + (-1,)) - - - -# The opposite of canonstr() -- sorta. I.e. it attempts to encode s in the -# charset of the given language, which is the character set that the page will -# be rendered in, and failing that, replaces non-ASCII characters with their -# html references. It always returns a byte string. -def uncanonstr(s, lang=None): - if s is None: - s = u'' - if lang is None: - charset = 'us-ascii' - else: - charset = GetCharSet(lang) - # See if the string contains characters only in the desired character - # set. If so, return it unchanged, except for coercing it to a byte - # string. - try: - if isinstance(s, unicode): - return s.encode(charset) - else: - u = unicode(s, charset) - return s - except UnicodeError: - # Nope, it contains funny characters, so html-ref it - return uquote(s) - - -def uquote(s): - a = [] - for c in s: - o = ord(c) - if o > 127: - a.append('&#%3d;' % o) - else: - a.append(c) - # Join characters together and coerce to byte string - return str(EMPTYSTRING.join(a)) - - -def oneline(s, cset='us-ascii', in_unicode=False): - # Decode header string in one line and convert into specified charset - try: - h = email.Header.make_header(email.Header.decode_header(s)) - ustr = h.__unicode__() - line = UEMPTYSTRING.join(ustr.splitlines()) - if in_unicode: - return line - else: - return line.encode(cset, 'replace') - except (LookupError, UnicodeError, ValueError, HeaderParseError): - # possibly charset problem. return with undecoded string in one line. - return EMPTYSTRING.join(s.splitlines()) - - -def strip_verbose_pattern(pattern): - # Remove white space and comments from a verbose pattern and return a - # non-verbose, equivalent pattern. Replace CR and NL in the result - # with '\\r' and '\\n' respectively to avoid multi-line results. - if not isinstance(pattern, str): - return pattern - newpattern = '' - i = 0 - inclass = False - skiptoeol = False - copynext = False - while i < len(pattern): - c = pattern[i] - if copynext: - if c == NL: - newpattern += '\\n' - elif c == CR: - newpattern += '\\r' - else: - newpattern += c - copynext = False - elif skiptoeol: - if c == NL: - skiptoeol = False - elif c == '#' and not inclass: - skiptoeol = True - elif c == '[' and not inclass: - inclass = True - newpattern += c - copynext = True - elif c == ']' and inclass: - inclass = False - newpattern += c - elif re.search('\s', c): - if inclass: - if c == NL: - newpattern += '\\n' - elif c == CR: - newpattern += '\\r' - else: - newpattern += c - elif c == '\\' and not inclass: - newpattern += c - copynext = True - else: - if c == NL: - newpattern += '\\n' - elif c == CR: - newpattern += '\\r' - else: - newpattern += c - i += 1 - return newpattern - - - -def get_pattern(email, pattern_list): - """Returns matched entry in pattern_list if email matches. - Otherwise returns None. - """ - if not pattern_list: - return None - matched = None - for pattern in pattern_list: - if pattern.startswith('^'): - # This is a regular expression match - try: - if re.search(pattern, email, re.IGNORECASE): - matched = pattern - break - except re.error: - # BAW: we should probably remove this pattern - pass - else: - # Do the comparison case insensitively - if pattern.lower() == email.lower(): - matched = pattern - break - return matched diff --git a/mailman/__init__.py b/mailman/__init__.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/mailman/app/__init__.py b/mailman/app/__init__.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/mailman/app/bounces.py b/mailman/app/bounces.py deleted file mode 100644 index 875f615a5..000000000 --- a/mailman/app/bounces.py +++ /dev/null @@ -1,63 +0,0 @@ -# Copyright (C) 2007-2009 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 . - -"""Application level bounce handling.""" - -from __future__ import unicode_literals - -__metaclass__ = type -__all__ = [ - 'bounce_message', - ] - -import logging - -from email.mime.message import MIMEMessage -from email.mime.text import MIMEText - -from mailman import Message -from mailman import Utils -from mailman.i18n import _ - -log = logging.getLogger('mailman.config') - - - -def bounce_message(mlist, msg, e=None): - # Bounce a message back to the sender, with an error message if provided - # in the exception argument. - sender = msg.get_sender() - subject = msg.get('subject', _('(no subject)')) - subject = Utils.oneline(subject, - Utils.GetCharSet(mlist.preferred_language)) - if e is None: - notice = _('[No bounce details are available]') - else: - notice = _(e.notice) - # Currently we always craft bounces as MIME messages. - bmsg = Message.UserNotification(msg.get_sender(), - mlist.owner_address, - subject, - lang=mlist.preferred_language) - # BAW: Be sure you set the type before trying to attach, or you'll get - # a MultipartConversionError. - bmsg.set_type('multipart/mixed') - txt = MIMEText(notice, - _charset=Utils.GetCharSet(mlist.preferred_language)) - bmsg.attach(txt) - bmsg.attach(MIMEMessage(msg)) - bmsg.send(mlist) diff --git a/mailman/app/commands.py b/mailman/app/commands.py deleted file mode 100644 index d7676af9c..000000000 --- a/mailman/app/commands.py +++ /dev/null @@ -1,44 +0,0 @@ -# Copyright (C) 2008-2009 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 . - -"""Initialize the email commands.""" - -from __future__ import unicode_literals - -__metaclass__ = type -__all__ = [ - 'initialize', - ] - - -from mailman.config import config -from mailman.core.plugins import get_plugins -from mailman.interfaces.command import IEmailCommand - - - -def initialize(): - """Initialize the email commands.""" - for module in get_plugins('mailman.commands'): - for name in module.__all__: - command_class = getattr(module, name) - if not IEmailCommand.implementedBy(command_class): - continue - assert command_class.name not in config.commands, ( - 'Duplicate email command "{0}" found in {1}'.format( - command_class.name, module)) - config.commands[command_class.name] = command_class() diff --git a/mailman/app/lifecycle.py b/mailman/app/lifecycle.py deleted file mode 100644 index eec00dc86..000000000 --- a/mailman/app/lifecycle.py +++ /dev/null @@ -1,114 +0,0 @@ -# Copyright (C) 2007-2009 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 . - -"""Application level list creation.""" - -from __future__ import unicode_literals - -__metaclass__ = type -__all__ = [ - 'create_list', - 'remove_list', - ] - - -import os -import sys -import shutil -import logging - -from mailman import Utils -from mailman.Utils import ValidateEmail -from mailman.config import config -from mailman.core import errors -from mailman.interfaces.member import MemberRole - - -log = logging.getLogger('mailman.error') - - - -def create_list(fqdn_listname, owners=None): - """Create the named list and apply styles.""" - if owners is None: - owners = [] - ValidateEmail(fqdn_listname) - listname, domain = fqdn_listname.split('@', 1) - if domain not in config.domains: - raise errors.BadDomainSpecificationError(domain) - mlist = config.db.list_manager.create(fqdn_listname) - for style in config.style_manager.lookup(mlist): - style.apply(mlist) - # Coordinate with the MTA, as defined in the configuration file. - module_name, class_name = config.mta.incoming.rsplit('.', 1) - __import__(module_name) - getattr(sys.modules[module_name], class_name)().create(mlist) - # Create any owners that don't yet exist, and subscribe all addresses as - # owners of the mailing list. - usermgr = config.db.user_manager - for owner_address in owners: - addr = usermgr.get_address(owner_address) - if addr is None: - # XXX Make this use an IRegistrar instead, but that requires - # sussing out the IDomain stuff. For now, fake it. - user = usermgr.create_user(owner_address) - addr = list(user.addresses)[0] - addr.subscribe(mlist, MemberRole.owner) - return mlist - - - -def remove_list(fqdn_listname, mailing_list=None, archives=True): - """Remove the list and all associated artifacts and subscriptions.""" - removeables = [] - # mailing_list will be None when only residual archives are being removed. - if mailing_list: - # Remove all subscriptions, regardless of role. - for member in mailing_list.subscribers.members: - member.unsubscribe() - # Delete the mailing list from the database. - config.db.list_manager.delete(mailing_list) - # Do the MTA-specific list deletion tasks - module_name, class_name = config.mta.incoming.rsplit('.', 1) - __import__(module_name) - getattr(sys.modules[module_name], class_name)().create(mailing_list) - # Remove the list directory. - removeables.append(os.path.join(config.LIST_DATA_DIR, fqdn_listname)) - # Remove any stale locks associated with the list. - for filename in os.listdir(config.LOCK_DIR): - fn_listname = filename.split('.')[0] - if fn_listname == fqdn_listname: - removeables.append(os.path.join(config.LOCK_DIR, filename)) - if archives: - private_dir = config.PRIVATE_ARCHIVE_FILE_DIR - public_dir = config.PUBLIC_ARCHIVE_FILE_DIR - removeables.extend([ - os.path.join(private_dir, fqdn_listname), - os.path.join(private_dir, fqdn_listname + '.mbox'), - os.path.join(public_dir, fqdn_listname), - os.path.join(public_dir, fqdn_listname + '.mbox'), - ]) - # Now that we know what files and directories to delete, delete them. - for target in removeables: - if os.path.islink(target): - os.unlink(target) - elif os.path.isdir(target): - shutil.rmtree(target) - elif os.path.isfile(target): - os.unlink(target) - else: - log.error('Could not delete list artifact: %s', target) diff --git a/mailman/app/membership.py b/mailman/app/membership.py deleted file mode 100644 index 4b9609469..000000000 --- a/mailman/app/membership.py +++ /dev/null @@ -1,137 +0,0 @@ -# Copyright (C) 2007-2009 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 . - -"""Application support for membership management.""" - -from __future__ import unicode_literals - -__metaclass__ = type -__all__ = [ - 'add_member', - 'delete_member', - ] - - -from email.utils import formataddr - -from mailman import Message -from mailman import Utils -from mailman import i18n -from mailman.app.notifications import send_goodbye_message -from mailman.config import config -from mailman.core import errors -from mailman.interfaces.member import AlreadySubscribedError, MemberRole - -_ = i18n._ - - - -def add_member(mlist, address, realname, password, delivery_mode, language): - """Add a member right now. - - The member's subscription must be approved by whatever policy the list - enforces. - - :param mlist: the mailing list to add the member to - :type mlist: IMailingList - :param address: the address to subscribe - :type address: string - :param realname: the subscriber's full name - :type realname: string - :param password: the subscriber's password - :type password: string - :param delivery_mode: the delivery mode the subscriber has chosen - :type delivery_mode: DeliveryMode - :param language: the language that the subscriber is going to use - :type language: string - """ - # Let's be extra cautious. - Utils.ValidateEmail(address) - if mlist.members.get_member(address) is not None: - raise AlreadySubscribedError( - mlist.fqdn_listname, address, MemberRole.member) - # Check for banned address here too for admin mass subscribes and - # confirmations. - pattern = Utils.get_pattern(address, mlist.ban_list) - if pattern: - raise errors.MembershipIsBanned(pattern) - # Do the actual addition. First, see if there's already a user linked - # with the given address. - user = config.db.user_manager.get_user(address) - if user is None: - # A user linked to this address does not yet exist. Is the address - # itself known but just not linked to a user? - address_obj = config.db.user_manager.get_address(address) - if address_obj is None: - # Nope, we don't even know about this address, so create both the - # user and address now. - user = config.db.user_manager.create_user(address, realname) - # Do it this way so we don't have to flush the previous change. - address_obj = list(user.addresses)[0] - else: - # The address object exists, but it's not linked to a user. - # Create the user and link it now. - user = config.db.user_manager.create_user() - user.real_name = (realname if realname else address_obj.real_name) - user.link(address_obj) - # Since created the user, then the member, and set preferences on the - # appropriate object. - user.password = password - user.preferences.preferred_language = language - member = address_obj.subscribe(mlist, MemberRole.member) - member.preferences.delivery_mode = delivery_mode - else: - # The user exists and is linked to the address. - for address_obj in user.addresses: - if address_obj.address == address: - break - else: - raise AssertionError( - 'User should have had linked address: {0}'.format(address)) - # Create the member and set the appropriate preferences. - member = address_obj.subscribe(mlist, MemberRole.member) - member.preferences.preferred_language = language - member.preferences.delivery_mode = delivery_mode -## mlist.setMemberOption(email, config.Moderate, -## mlist.default_member_moderation) - - - -def delete_member(mlist, address, admin_notif=None, userack=None): - if userack is None: - userack = mlist.send_goodbye_msg - if admin_notif is None: - admin_notif = mlist.admin_notify_mchanges - # Delete a member, for which we know the approval has been made - member = mlist.members.get_member(address) - language = member.preferred_language - member.unsubscribe() - # And send an acknowledgement to the user... - if userack: - send_goodbye_message(mlist, address, language) - # ...and to the administrator. - if admin_notif: - user = config.db.user_manager.get_user(address) - realname = user.real_name - subject = _('$mlist.real_name unsubscription notification') - text = Utils.maketext( - 'adminunsubscribeack.txt', - {'listname': mlist.real_name, - 'member' : formataddr((realname, address)), - }, mlist=mlist) - msg = Message.OwnerNotification(mlist, subject, text) - msg.send(mlist) diff --git a/mailman/app/moderator.py b/mailman/app/moderator.py deleted file mode 100644 index b40a34344..000000000 --- a/mailman/app/moderator.py +++ /dev/null @@ -1,351 +0,0 @@ -# Copyright (C) 2007-2009 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 . - -"""Application support for moderators.""" - -from __future__ import unicode_literals - -__metaclass__ = type -__all__ = [ - 'handle_message', - 'handle_subscription', - 'handle_unsubscription', - 'hold_message', - 'hold_subscription', - 'hold_unsubscription', - ] - -import logging - -from datetime import datetime -from email.utils import formataddr, formatdate, getaddresses, make_msgid - -from mailman import Message -from mailman import Utils -from mailman import i18n -from mailman.app.membership import add_member, delete_member -from mailman.app.notifications import ( - send_admin_subscription_notice, send_welcome_message) -from mailman.config import config -from mailman.core import errors -from mailman.interfaces import Action -from mailman.interfaces.member import AlreadySubscribedError, DeliveryMode -from mailman.interfaces.requests import RequestType - -_ = i18n._ - -vlog = logging.getLogger('mailman.vette') -slog = logging.getLogger('mailman.subscribe') - - - -def hold_message(mlist, msg, msgdata=None, reason=None): - """Hold a message for moderator approval. - - The message is added to the mailing list's request database. - - :param mlist: The mailing list to hold the message on. - :param msg: The message to hold. - :param msgdata: Optional message metadata to hold. If not given, a new - metadata dictionary is created and held with the message. - :param reason: Optional string reason why the message is being held. If - not given, the empty string is used. - :return: An id used to handle the held message later. - """ - if msgdata is None: - msgdata = {} - else: - # Make a copy of msgdata so that subsequent changes won't corrupt the - # request database. TBD: remove the `filebase' key since this will - # not be relevant when the message is resurrected. - msgdata = msgdata.copy() - if reason is None: - reason = '' - # Add the message to the message store. It is required to have a - # Message-ID header. - message_id = msg.get('message-id') - if message_id is None: - msg['Message-ID'] = message_id = unicode(make_msgid()) - assert isinstance(message_id, unicode), ( - 'Message-ID is not a unicode: %s' % message_id) - config.db.message_store.add(msg) - # Prepare the message metadata with some extra information needed only by - # the moderation interface. - msgdata['_mod_message_id'] = message_id - msgdata['_mod_fqdn_listname'] = mlist.fqdn_listname - msgdata['_mod_sender'] = msg.get_sender() - msgdata['_mod_subject'] = msg.get('subject', _('(no subject)')) - msgdata['_mod_reason'] = reason - msgdata['_mod_hold_date'] = datetime.now().isoformat() - # Now hold this request. We'll use the message_id as the key. - requestsdb = config.db.requests.get_list_requests(mlist) - request_id = requestsdb.hold_request( - RequestType.held_message, message_id, msgdata) - return request_id - - - -def handle_message(mlist, id, action, - comment=None, preserve=False, forward=None): - requestdb = config.db.requests.get_list_requests(mlist) - key, msgdata = requestdb.get_request(id) - # Handle the action. - rejection = None - message_id = msgdata['_mod_message_id'] - sender = msgdata['_mod_sender'] - subject = msgdata['_mod_subject'] - if action is Action.defer: - # Nothing to do, but preserve the message for later. - preserve = True - elif action is Action.discard: - rejection = 'Discarded' - elif action is Action.reject: - rejection = 'Refused' - member = mlist.members.get_member(sender) - if member: - language = member.preferred_language - else: - language = None - _refuse(mlist, _('Posting of your message titled "$subject"'), - sender, comment or _('[No reason given]'), language) - elif action is Action.accept: - # Start by getting the message from the message store. - msg = config.db.message_store.get_message_by_id(message_id) - # Delete moderation-specific entries from the message metadata. - for key in msgdata.keys(): - if key.startswith('_mod_'): - del msgdata[key] - # Add some metadata to indicate this message has now been approved. - msgdata['approved'] = True - msgdata['moderator_approved'] = True - # Calculate a new filebase for the approved message, otherwise - # delivery errors will cause duplicates. - if 'filebase' in msgdata: - del msgdata['filebase'] - # Queue the file for delivery by qrunner. Trying to deliver the - # message directly here can lead to a huge delay in web turnaround. - # Log the moderation and add a header. - msg['X-Mailman-Approved-At'] = formatdate(localtime=True) - vlog.info('held message approved, message-id: %s', - msg.get('message-id', 'n/a')) - # Stick the message back in the incoming queue for further - # processing. - config.switchboards['in'].enqueue(msg, _metadata=msgdata) - else: - raise AssertionError('Unexpected action: {0}'.format(action)) - # Forward the message. - if forward: - # Get a copy of the original message from the message store. - msg = config.db.message_store.get_message_by_id(message_id) - # It's possible the forwarding address list is a comma separated list - # of realname/address pairs. - addresses = [addr[1] for addr in getaddresses(forward)] - language = mlist.preferred_language - if len(addresses) == 1: - # If the address getting the forwarded message is a member of - # the list, we want the headers of the outer message to be - # encoded in their language. Otherwise it'll be the preferred - # language of the mailing list. This is better than sending a - # separate message per recipient. - member = mlist.members.get_member(addresses[0]) - if member: - language = member.preferred_language - with i18n.using_language(language): - fmsg = Message.UserNotification( - addresses, mlist.bounces_address, - _('Forward of moderated message'), - lang=language) - fmsg.set_type('message/rfc822') - fmsg.attach(msg) - fmsg.send(mlist) - # Delete the message from the message store if it is not being preserved. - if not preserve: - config.db.message_store.delete_message(message_id) - requestdb.delete_request(id) - # Log the rejection - if rejection: - note = """%s: %s posting: -\tFrom: %s -\tSubject: %s""" - if comment: - note += '\n\tReason: ' + comment - vlog.info(note, mlist.fqdn_listname, rejection, sender, subject) - - - -def hold_subscription(mlist, address, realname, password, mode, language): - data = dict(when=datetime.now().isoformat(), - address=address, - realname=realname, - password=password, - delivery_mode=str(mode), - language=language) - # Now hold this request. We'll use the address as the key. - requestsdb = config.db.requests.get_list_requests(mlist) - request_id = requestsdb.hold_request( - RequestType.subscription, address, data) - vlog.info('%s: held subscription request from %s', - mlist.fqdn_listname, address) - # Possibly notify the administrator in default list language - if mlist.admin_immed_notify: - subject = _( - 'New subscription request to list $mlist.real_name from $address') - text = Utils.maketext( - 'subauth.txt', - {'username' : address, - 'listname' : mlist.fqdn_listname, - 'admindb_url': mlist.script_url('admindb'), - }, mlist=mlist) - # This message should appear to come from the -owner so as - # to avoid any useless bounce processing. - msg = Message.UserNotification( - mlist.owner_address, mlist.owner_address, - subject, text, mlist.preferred_language) - msg.send(mlist, tomoderators=True) - return request_id - - - -def handle_subscription(mlist, id, action, comment=None): - requestdb = config.db.requests.get_list_requests(mlist) - if action is Action.defer: - # Nothing to do. - return - elif action is Action.discard: - # Nothing to do except delete the request from the database. - pass - elif action is Action.reject: - key, data = requestdb.get_request(id) - _refuse(mlist, _('Subscription request'), - data['address'], - comment or _('[No reason given]'), - lang=data['language']) - elif action is Action.accept: - key, data = requestdb.get_request(id) - enum_value = data['delivery_mode'].split('.')[-1] - delivery_mode = DeliveryMode(enum_value) - address = data['address'] - realname = data['realname'] - language = data['language'] - password = data['password'] - try: - add_member(mlist, address, realname, password, - delivery_mode, language) - except AlreadySubscribedError: - # The address got subscribed in some other way after the original - # request was made and accepted. - pass - else: - if mlist.send_welcome_msg: - send_welcome_message(mlist, address, language, delivery_mode) - if mlist.admin_notify_mchanges: - send_admin_subscription_notice( - mlist, address, realname, language) - slog.info('%s: new %s, %s %s', mlist.fqdn_listname, - delivery_mode, formataddr((realname, address)), - 'via admin approval') - else: - raise AssertionError('Unexpected action: {0}'.format(action)) - # Delete the request from the database. - requestdb.delete_request(id) - - - -def hold_unsubscription(mlist, address): - data = dict(address=address) - requestsdb = config.db.requests.get_list_requests(mlist) - request_id = requestsdb.hold_request( - RequestType.unsubscription, address, data) - vlog.info('%s: held unsubscription request from %s', - mlist.fqdn_listname, address) - # Possibly notify the administrator of the hold - if mlist.admin_immed_notify: - subject = _( - 'New unsubscription request from $mlist.real_name by $address') - text = Utils.maketext( - 'unsubauth.txt', - {'address' : address, - 'listname' : mlist.fqdn_listname, - 'admindb_url': mlist.script_url('admindb'), - }, mlist=mlist) - # This message should appear to come from the -owner so as - # to avoid any useless bounce processing. - msg = Message.UserNotification( - mlist.owner_address, mlist.owner_address, - subject, text, mlist.preferred_language) - msg.send(mlist, tomoderators=True) - return request_id - - - -def handle_unsubscription(mlist, id, action, comment=None): - requestdb = config.db.requests.get_list_requests(mlist) - key, data = requestdb.get_request(id) - address = data['address'] - if action is Action.defer: - # Nothing to do. - return - elif action is Action.discard: - # Nothing to do except delete the request from the database. - pass - elif action is Action.reject: - key, data = requestdb.get_request(id) - _refuse(mlist, _('Unsubscription request'), address, - comment or _('[No reason given]')) - elif action is Action.accept: - key, data = requestdb.get_request(id) - try: - delete_member(mlist, address) - except errors.NotAMemberError: - # User has already been unsubscribed. - pass - slog.info('%s: deleted %s', mlist.fqdn_listname, address) - else: - raise AssertionError('Unexpected action: {0}'.format(action)) - # Delete the request from the database. - requestdb.delete_request(id) - - - -def _refuse(mlist, request, recip, comment, origmsg=None, lang=None): - # As this message is going to the requester, try to set the language to - # his/her language choice, if they are a member. Otherwise use the list's - # preferred language. - realname = mlist.real_name - if lang is None: - member = mlist.members.get_member(recip) - if member: - lang = member.preferred_language - text = Utils.maketext( - 'refuse.txt', - {'listname' : mlist.fqdn_listname, - 'request' : request, - 'reason' : comment, - 'adminaddr': mlist.owner_address, - }, lang=lang, mlist=mlist) - with i18n.using_language(lang): - # add in original message, but not wrap/filled - if origmsg: - text = NL.join( - [text, - '---------- ' + _('Original Message') + ' ----------', - str(origmsg) - ]) - subject = _('Request to mailing list "$realname" rejected') - msg = Message.UserNotification(recip, mlist.bounces_address, - subject, text, lang) - msg.send(mlist) diff --git a/mailman/app/notifications.py b/mailman/app/notifications.py deleted file mode 100644 index 9bef9998b..000000000 --- a/mailman/app/notifications.py +++ /dev/null @@ -1,136 +0,0 @@ -# Copyright (C) 2007-2009 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 . - -"""Sending notifications.""" - -from __future__ import unicode_literals - -__metaclass__ = type -__all__ = [ - 'send_admin_subscription_notice', - 'send_goodbye_message', - 'send_welcome_message', - ] - - -from email.utils import formataddr -from lazr.config import as_boolean - -from mailman import Message -from mailman import Utils -from mailman import i18n -from mailman.config import config -from mailman.interfaces.member import DeliveryMode - - -_ = i18n._ - - - -def send_welcome_message(mlist, address, language, delivery_mode, text=''): - """Send a welcome message to a subscriber. - - Prepending to the standard welcome message template is the mailing list's - welcome message, if there is one. - - :param mlist: the mailing list - :type mlist: IMailingList - :param address: The address to respond to - :type address: string - :param language: the language of the response - :type language: string - :param delivery_mode: the type of delivery the subscriber is getting - :type delivery_mode: DeliveryMode - """ - if mlist.welcome_msg: - welcome = Utils.wrap(mlist.welcome_msg) + '\n' - else: - welcome = '' - # Find the IMember object which is subscribed to the mailing list, because - # from there, we can get the member's options url. - member = mlist.members.get_member(address) - options_url = member.options_url - # Get the text from the template. - text += Utils.maketext( - 'subscribeack.txt', { - 'real_name' : mlist.real_name, - 'posting_address' : mlist.fqdn_listname, - 'listinfo_url' : mlist.script_url('listinfo'), - 'optionsurl' : options_url, - 'request_address' : mlist.request_address, - 'welcome' : welcome, - }, lang=language, mlist=mlist) - if delivery_mode is not DeliveryMode.regular: - digmode = _(' (Digest mode)') - else: - digmode = '' - msg = Message.UserNotification( - address, mlist.request_address, - _('Welcome to the "$mlist.real_name" mailing list${digmode}'), - text, language) - msg['X-No-Archive'] = 'yes' - msg.send(mlist, verp=as_boolean(config.mta.verp_personalized_deliveries)) - - - -def send_goodbye_message(mlist, address, language): - """Send a goodbye message to a subscriber. - - Prepending to the standard goodbye message template is the mailing list's - goodbye message, if there is one. - - :param mlist: the mailing list - :type mlist: IMailingList - :param address: The address to respond to - :type address: string - :param language: the language of the response - :type language: string - """ - if mlist.goodbye_msg: - goodbye = Utils.wrap(mlist.goodbye_msg) + '\n' - else: - goodbye = '' - msg = Message.UserNotification( - address, mlist.bounces_address, - _('You have been unsubscribed from the $mlist.real_name mailing list'), - goodbye, language) - msg.send(mlist, verp=as_boolean(config.mta.verp_personalized_deliveries)) - - - -def send_admin_subscription_notice(mlist, address, full_name, language): - """Send the list administrators a subscription notice. - - :param mlist: the mailing list - :type mlist: IMailingList - :param address: the address being subscribed - :type address: string - :param full_name: the name of the subscriber - :type full_name: string - :param language: the language of the address's realname - :type language: string - """ - with i18n.using_language(mlist.preferred_language): - subject = _('$mlist.real_name subscription notification') - full_name = full_name.encode(Utils.GetCharSet(language), 'replace') - text = Utils.maketext( - 'adminsubscribeack.txt', - {'listname' : mlist.real_name, - 'member' : formataddr((full_name, address)), - }, mlist=mlist) - msg = Message.OwnerNotification(mlist, subject, text) - msg.send(mlist) diff --git a/mailman/app/registrar.py b/mailman/app/registrar.py deleted file mode 100644 index 6a2abeba9..000000000 --- a/mailman/app/registrar.py +++ /dev/null @@ -1,163 +0,0 @@ -# Copyright (C) 2007-2009 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 . - -"""Implementation of the IUserRegistrar interface.""" - -from __future__ import unicode_literals - -__metaclass__ = type -__all__ = [ - 'Registrar', - 'adapt_domain_to_registrar', - ] - - -import datetime - -from pkg_resources import resource_string -from zope.interface import implements - -from mailman.Message import UserNotification -from mailman.Utils import ValidateEmail -from mailman.config import config -from mailman.i18n import _ -from mailman.interfaces.domain import IDomain -from mailman.interfaces.member import MemberRole -from mailman.interfaces.pending import IPendable -from mailman.interfaces.registrar import IRegistrar - - - -class PendableRegistration(dict): - implements(IPendable) - PEND_KEY = 'registration' - - - -class Registrar: - implements(IRegistrar) - - def __init__(self, context): - self._context = context - - def register(self, address, real_name=None, mlist=None): - """See `IUserRegistrar`.""" - # First, do validation on the email address. If the address is - # invalid, it will raise an exception, otherwise it just returns. - ValidateEmail(address) - # Create a pendable for the registration. - pendable = PendableRegistration( - type=PendableRegistration.PEND_KEY, - address=address, - real_name=real_name) - if mlist is not None: - pendable['list_name'] = mlist.fqdn_listname - token = config.db.pendings.add(pendable) - # Set up some local variables for translation interpolation. - domain = IDomain(self._context) - domain_name = _(domain.email_host) - contact_address = domain.contact_address - confirm_url = domain.confirm_url(token) - confirm_address = domain.confirm_address(token) - email_address = address - # Calculate the message's Subject header. XXX Have to deal with - # translating this subject header properly. XXX Must deal with - # VERP_CONFIRMATIONS as well. - subject = 'confirm ' + token - # Send a verification email to the address. - text = _(resource_string('mailman.templates.en', 'verify.txt')) - msg = UserNotification(address, confirm_address, subject, text) - msg.send(mlist=None) - return token - - def confirm(self, token): - """See `IUserRegistrar`.""" - # For convenience - pendable = config.db.pendings.confirm(token) - if pendable is None: - return False - missing = object() - address = pendable.get('address', missing) - real_name = pendable.get('real_name', missing) - list_name = pendable.get('list_name', missing) - if pendable.get('type') != PendableRegistration.PEND_KEY: - # It seems like it would be very difficult to accurately guess - # tokens, or brute force an attack on the SHA1 hash, so we'll just - # throw the pendable away in that case. It's possible we'll need - # to repend the event or adjust the API to handle this case - # better, but for now, the simpler the better. - return False - # We are going to end up with an IAddress for the verified address - # and an IUser linked to this IAddress. See if any of these objects - # currently exist in our database. - usermgr = config.db.user_manager - addr = (usermgr.get_address(address) - if address is not missing else None) - user = (usermgr.get_user(address) - if address is not missing else None) - # If there is neither an address nor a user matching the confirmed - # record, then create the user, which will in turn create the address - # and link the two together - if addr is None: - assert user is None, 'How did we get a user but not an address?' - user = usermgr.create_user(address, real_name) - # Because the database changes haven't been flushed, we can't use - # IUserManager.get_address() to find the IAddress just created - # under the hood. Instead, iterate through the IUser's addresses, - # of which really there should be only one. - for addr in user.addresses: - if addr.address == address: - break - else: - raise AssertionError('Could not find expected IAddress') - elif user is None: - user = usermgr.create_user() - user.real_name = real_name - user.link(addr) - else: - # The IAddress and linked IUser already exist, so all we need to - # do is verify the address. - pass - addr.verified_on = datetime.datetime.now() - # If this registration is tied to a mailing list, subscribe the person - # to the list right now. - list_name = pendable.get('list_name') - if list_name is not None: - mlist = config.db.list_manager.get(list_name) - if mlist: - addr.subscribe(mlist, MemberRole.member) - return True - - def discard(self, token): - # Throw the record away. - config.db.pendings.confirm(token) - - - -def adapt_domain_to_registrar(iface, obj): - """Adapt `IDomain` to `IRegistrar`. - - :param iface: The interface to adapt to. - :type iface: `zope.interface.Interface` - :param obj: The object being adapted. - :type obj: `IDomain` - :return: An `IRegistrar` instance if adaptation succeeded or None if it - didn't. - """ - return (Registrar(obj) - if IDomain.providedBy(obj) and iface is IRegistrar - else None) diff --git a/mailman/app/replybot.py b/mailman/app/replybot.py deleted file mode 100644 index 0537f6645..000000000 --- a/mailman/app/replybot.py +++ /dev/null @@ -1,125 +0,0 @@ -# Copyright (C) 2007-2009 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 . - -"""Application level auto-reply code.""" - -# XXX This should undergo a rewrite to move this functionality off of the -# mailing list. The reply governor should really apply site-wide per -# recipient (I think). - -from __future__ import unicode_literals - -__metaclass__ = type -__all__ = [ - 'autorespond_to_sender', - 'can_acknowledge', - ] - -import logging -import datetime - -from mailman import Utils -from mailman import i18n -from mailman.config import config - - -log = logging.getLogger('mailman.vette') -_ = i18n._ - - - -def autorespond_to_sender(mlist, sender, lang=None): - """Return True if Mailman should auto-respond to this sender. - - This is only consulted for messages sent to the -request address, or - for posting hold notifications, and serves only as a safety value for - mail loops with email 'bots. - """ - if lang is None: - lang = mlist.preferred_language - max_autoresponses_per_day = int(config.mta.max_autoresponses_per_day) - if max_autoresponses_per_day == 0: - # Unlimited. - return True - today = datetime.date.today() - info = mlist.hold_and_cmd_autoresponses.get(sender) - if info is None or info[0] <> today: - # This is the first time we've seen a -request/post-hold for this - # sender today. - mlist.hold_and_cmd_autoresponses[sender] = (today, 1) - return True - date, count = info - if count < 0: - # They've already hit the limit for today, and we've already notified - # them of this fact, so there's nothing more to do. - log.info('-request/hold autoresponse discarded for: %s', sender) - return False - if count >= max_autoresponses_per_day: - log.info('-request/hold autoresponse limit hit for: %s', sender) - mlist.hold_and_cmd_autoresponses[sender] = (today, -1) - # Send this notification message instead. - text = Utils.maketext( - 'nomoretoday.txt', - {'sender' : sender, - 'listname': mlist.fqdn_listname, - 'num' : count, - 'owneremail': mlist.owner_address, - }, - lang=lang) - with i18n.using_language(lang): - msg = Message.UserNotification( - sender, mlist.owner_address, - _('Last autoresponse notification for today'), - text, lang=lang) - msg.send(mlist) - return False - mlist.hold_and_cmd_autoresponses[sender] = (today, count + 1) - return True - - - -def can_acknowledge(msg): - """A boolean specifying whether this message can be acknowledged. - - There are several reasons why a message should not be acknowledged, mostly - related to competing standards or common practices. These include: - - * The message has a X-No-Ack header with any value - * The message has an X-Ack header with a 'no' value - * The message has a Precedence header - * The message has an Auto-Submitted header and that header does not have a - value of 'no' - * The message has an empty Return-Path header, e.g. <> - * The message has any RFC 2369 headers (i.e. List-* headers) - - :param msg: a Message object. - :return: Boolean specifying whether the message can be acknowledged or not - (which is different from whether it will be acknowledged). - """ - # I wrote it this way for clarity and consistency with the docstring. - for header in msg.keys(): - if header in ('x-no-ack', 'precedence'): - return False - if header.lower().startswith('list-'): - return False - if msg.get('x-ack', '').lower() == 'no': - return False - if msg.get('auto-submitted', 'no').lower() <> 'no': - return False - if msg.get('return-path') == '<>': - return False - return True diff --git a/mailman/archiving/__init__.py b/mailman/archiving/__init__.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/mailman/archiving/mailarchive.py b/mailman/archiving/mailarchive.py deleted file mode 100644 index a5eb27db0..000000000 --- a/mailman/archiving/mailarchive.py +++ /dev/null @@ -1,87 +0,0 @@ -# Copyright (C) 2008-2009 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 . - -"""The Mail-Archive.com archiver.""" - -from __future__ import absolute_import, unicode_literals - -__metaclass__ = type -__all__ = [ - 'MailArchive', - ] - - -import hashlib - -from base64 import urlsafe_b64encode -from urllib import quote -from urlparse import urljoin -from zope.interface import implements - -from mailman.config import config -from mailman.interfaces.archiver import IArchiver - - - -class MailArchive: - """Public archiver at the Mail-Archive.com. - - Messages get archived at http://go.mail-archive.com. - """ - - implements(IArchiver) - - name = 'mail-archive' - - @staticmethod - def list_url(mlist): - """See `IArchiver`.""" - if mlist.archive_private: - return None - return urljoin(config.archiver.mail_archive.base_url, - quote(mlist.posting_address)) - - @staticmethod - def permalink(mlist, msg): - """See `IArchiver`.""" - if mlist.archive_private: - return None - message_id = msg.get('message-id') - # It is not the archiver's job to ensure the message has a Message-ID. - # If no Message-ID is available, there is no permalink. - if message_id is None: - return None - # The angle brackets are not part of the Message-ID. See RFC 2822. - if message_id.startswith('<') and message_id.endswith('>'): - message_id = message_id[1:-1] - else: - message_id = message_id.strip() - sha = hashlib.sha1(message_id) - sha.update(str(mlist.posting_address)) - message_id_hash = urlsafe_b64encode(sha.digest()) - del msg['x-message-id-hash'] - msg['X-Message-ID-Hash'] = message_id_hash - return urljoin(config.archiver.mail_archive.base_url, message_id_hash) - - @staticmethod - def archive_message(mlist, msg): - """See `IArchiver`.""" - if not mlist.archive_private: - config.switchboards['out'].enqueue( - msg, - listname=mlist.fqdn_listname, - recips=[config.archiver.mail_archive.recipient]) diff --git a/mailman/archiving/mhonarc.py b/mailman/archiving/mhonarc.py deleted file mode 100644 index 949a79144..000000000 --- a/mailman/archiving/mhonarc.py +++ /dev/null @@ -1,97 +0,0 @@ -# Copyright (C) 2008-2009 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 . - -"""MHonArc archiver.""" - -from __future__ import absolute_import, unicode_literals - -__metaclass__ = type -__all__ = [ - 'MHonArc', - ] - - -import hashlib -import logging -import subprocess - -from base64 import b32encode -from urlparse import urljoin -from zope.interface import implements - -from mailman.config import config -from mailman.interfaces.archiver import IArchiver -from mailman.utilities.string import expand - - -log = logging.getLogger('mailman.archiver') - - - -class MHonArc: - """Local MHonArc archiver.""" - - implements(IArchiver) - - name = 'mhonarc' - - @staticmethod - def list_url(mlist): - """See `IArchiver`.""" - # XXX What about private MHonArc archives? - web_host = config.domains[mlist.host_name].url_host - return expand(config.archiver.mhonarc.base_url, - dict(listname=mlist.fqdn_listname, - hostname=web_host, - fqdn_listname=mlist.fqdn_listname, - )) - - @staticmethod - def permalink(mlist, msg): - """See `IArchiver`.""" - # XXX What about private MHonArc archives? - message_id = msg.get('message-id') - # It is not the archiver's job to ensure the message has a Message-ID. - # If no Message-ID is available, there is no permalink. - if message_id is None: - return None - # The angle brackets are not part of the Message-ID. See RFC 2822. - if message_id.startswith('<') and message_id.endswith('>'): - message_id = message_id[1:-1] - else: - message_id = message_id.strip() - sha = hashlib.sha1(message_id) - message_id_hash = b32encode(sha.digest()) - del msg['x-message-id-hash'] - msg['X-Message-ID-Hash'] = message_id_hash - return urljoin(MHonArc.list_url(mlist), message_id_hash) - - @staticmethod - def archive_message(mlist, msg): - """See `IArchiver`.""" - substitutions = config.__dict__.copy() - substitutions['listname'] = mlist.fqdn_listname - command = expand(config.archiver.mhonarc.command, substitutions) - proc = subprocess.Popen( - command, stdout=subprocess.PIPE, stderr=subprocess.PIPE, - shell=True) - stdout, stderr = proc.communicate(msg.as_string()) - if proc.returncode <> 0: - log.error('%s: mhonarc subprocess had non-zero exit code: %s' % - (msg['message-id'], proc.returncode)) - log.info(stdout) - log.error(stderr) diff --git a/mailman/archiving/pipermail.py b/mailman/archiving/pipermail.py deleted file mode 100644 index 377f4ab53..000000000 --- a/mailman/archiving/pipermail.py +++ /dev/null @@ -1,121 +0,0 @@ -# Copyright (C) 2007-2009 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 . - -"""Pipermail archiver.""" - -from __future__ import absolute_import, unicode_literals - -__metaclass__ = type -__all__ = [ - 'Pipermail', - ] - - -import os - -from cStringIO import StringIO -from zope.interface import implements -from zope.interface.interface import adapter_hooks - -from mailman.config import config -from mailman.interfaces.archiver import IArchiver, IPipermailMailingList -from mailman.interfaces.mailinglist import IMailingList -from mailman.utilities.filesystem import makedirs -from mailman.utilities.string import expand - -from mailman.Archiver.HyperArch import HyperArchive - - - -class PipermailMailingListAdapter: - """An adapter for MailingList objects to work with Pipermail.""" - - implements(IPipermailMailingList) - - def __init__(self, mlist): - self._mlist = mlist - - def __getattr__(self, name): - return getattr(self._mlist, name) - - def archive_dir(self): - """See `IPipermailMailingList`.""" - if self._mlist.archive_private: - basedir = config.PRIVATE_ARCHIVE_FILE_DIR - else: - basedir = config.PUBLIC_ARCHIVE_FILE_DIR - # Make sure the archive directory exists. - archive_dir = os.path.join(basedir, self._mlist.fqdn_listname) - makedirs(archive_dir) - return archive_dir - - -def adapt_mailing_list_for_pipermail(iface, obj): - """Adapt `IMailingLists` to `IPipermailMailingList`. - - :param iface: The interface to adapt to. - :type iface: `zope.interface.Interface` - :param obj: The object being adapted. - :type obj: any object - :return: An `IPipermailMailingList` instance if adaptation succeeded or - None if it didn't. - """ - return (PipermailMailingListAdapter(obj) - if IMailingList.providedBy(obj) and iface is IPipermailMailingList - else None) - -adapter_hooks.append(adapt_mailing_list_for_pipermail) - - - -class Pipermail: - """The stock Pipermail archiver.""" - - implements(IArchiver) - - name = 'pipermail' - - @staticmethod - def list_url(mlist): - """See `IArchiver`.""" - if mlist.archive_private: - url = mlist.script_url('private') + '/index.html' - else: - web_host = config.domains[mlist.host_name].url_host - return expand(config.archiver.pipermail.base_url, - dict(listname=mlist.fqdn_listname, - hostname=web_host, - fqdn_listname=mlist.fqdn_listname, - )) - - @staticmethod - def permalink(mlist, message): - """See `IArchiver`.""" - # Not currently implemented. - return None - - @staticmethod - def archive_message(mlist, message): - """See `IArchiver`.""" - text = str(message) - fileobj = StringIO(text) - h = HyperArchive(IPipermailMailingList(mlist)) - h.processUnixMailbox(fileobj) - h.close() - fileobj.close() - # There's no good way to know the url for the archived message. - return None diff --git a/mailman/archiving/prototype.py b/mailman/archiving/prototype.py deleted file mode 100644 index 81163e184..000000000 --- a/mailman/archiving/prototype.py +++ /dev/null @@ -1,77 +0,0 @@ -# Copyright (C) 2008-2009 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 . - -"""Prototypical permalinking archiver.""" - -from __future__ import absolute_import, unicode_literals - -__metaclass__ = type -__all__ = [ - 'Prototype', - ] - - -import hashlib - -from base64 import b32encode -from urlparse import urljoin -from zope.interface import implements - -from mailman.config import config -from mailman.interfaces.archiver import IArchiver - - - -class Prototype: - """A prototype of a third party archiver. - - Mailman proposes a draft specification for interoperability between list - servers and archivers: . - """ - - implements(IArchiver) - - name = 'prototype' - - @staticmethod - def list_url(mlist): - """See `IArchiver`.""" - return config.domains[mlist.host_name].base_url - - @staticmethod - def permalink(mlist, msg): - """See `IArchiver`.""" - message_id = msg.get('message-id') - # It is not the archiver's job to ensure the message has a Message-ID. - # If this header is missing, there is no permalink. - if message_id is None: - return None - # The angle brackets are not part of the Message-ID. See RFC 2822. - if message_id.startswith('<') and message_id.endswith('>'): - message_id = message_id[1:-1] - else: - message_id = message_id.strip() - digest = hashlib.sha1(message_id).digest() - message_id_hash = b32encode(digest) - del msg['x-message-id-hash'] - msg['X-Message-ID-Hash'] = message_id_hash - return urljoin(Prototype.list_url(mlist), message_id_hash) - - @staticmethod - def archive_message(mlist, message): - """See `IArchiver`.""" - raise NotImplementedError diff --git a/mailman/attic/Bouncer.py b/mailman/attic/Bouncer.py deleted file mode 100644 index e2de3c915..000000000 --- a/mailman/attic/Bouncer.py +++ /dev/null @@ -1,250 +0,0 @@ -# Copyright (C) 1998-2009 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 . - -"""Handle delivery bounces.""" - -import sys -import time -import logging - -from email.MIMEMessage import MIMEMessage -from email.MIMEText import MIMEText - -from mailman import Defaults -from mailman import Message -from mailman import Utils -from mailman import i18n -from mailman.configuration import config -from mailman.interfaces import DeliveryStatus - -EMPTYSTRING = '' - -# This constant is supposed to represent the day containing the first midnight -# after the epoch. We'll add (0,)*6 to this tuple to get a value appropriate -# for time.mktime(). -ZEROHOUR_PLUSONEDAY = time.localtime(60 * 60 * 24)[:3] - -def _(s): return s - -REASONS = { - DeliveryStatus.by_bounces : _('due to excessive bounces'), - DeliveryStatus.by_user : _('by yourself'), - DeliveryStatus.by_moderator : _('by the list administrator'), - DeliveryStatus.unknown : _('for unknown reasons'), - } - -_ = i18n._ - -log = logging.getLogger('mailman.bounce') -slog = logging.getLogger('mailman.subscribe') - - - -class _BounceInfo: - def __init__(self, member, score, date, noticesleft): - self.member = member - self.cookie = None - self.reset(score, date, noticesleft) - - def reset(self, score, date, noticesleft): - self.score = score - self.date = date - self.noticesleft = noticesleft - self.lastnotice = ZEROHOUR_PLUSONEDAY - - def __repr__(self): - # For debugging - return """\ -""" % self.__dict__ - - - -class Bouncer: - def registerBounce(self, member, msg, weight=1.0, day=None): - if not self.isMember(member): - return - info = self.getBounceInfo(member) - if day is None: - # Use today's date - day = time.localtime()[:3] - if not isinstance(info, _BounceInfo): - # This is the first bounce we've seen from this member - info = _BounceInfo(member, weight, day, - self.bounce_you_are_disabled_warnings) - self.setBounceInfo(member, info) - log.info('%s: %s bounce score: %s', self.internal_name(), - member, info.score) - # Continue to the check phase below - elif self.getDeliveryStatus(member) <> DeliveryStatus.enabled: - # The user is already disabled, so we can just ignore subsequent - # bounces. These are likely due to residual messages that were - # sent before disabling the member, but took a while to bounce. - log.info('%s: %s residual bounce received', - self.internal_name(), member) - return - elif info.date == day: - # We've already scored any bounces for this day, so ignore it. - log.info('%s: %s already scored a bounce for date %s', - self.internal_name(), member, - time.strftime('%d-%b-%Y', day + (0,0,0,0,1,0))) - # Continue to check phase below - else: - # See if this member's bounce information is stale. - now = Utils.midnight(day) - lastbounce = Utils.midnight(info.date) - if lastbounce + self.bounce_info_stale_after < now: - # Information is stale, so simply reset it - info.reset(weight, day, self.bounce_you_are_disabled_warnings) - log.info('%s: %s has stale bounce info, resetting', - self.internal_name(), member) - else: - # Nope, the information isn't stale, so add to the bounce - # score and take any necessary action. - info.score += weight - info.date = day - log.info('%s: %s current bounce score: %s', - self.internal_name(), member, info.score) - # Continue to the check phase below - # - # Now that we've adjusted the bounce score for this bounce, let's - # check to see if the disable-by-bounce threshold has been reached. - if info.score >= self.bounce_score_threshold: - if config.VERP_PROBES: - log.info('sending %s list probe to: %s (score %s >= %s)', - self.internal_name(), member, info.score, - self.bounce_score_threshold) - self.sendProbe(member, msg) - info.reset(0, info.date, info.noticesleft) - else: - self.disableBouncingMember(member, info, msg) - - def disableBouncingMember(self, member, info, msg): - # Initialize their confirmation cookie. If we do it when we get the - # first bounce, it'll expire by the time we get the disabling bounce. - cookie = self.pend_new(Pending.RE_ENABLE, self.internal_name(), member) - info.cookie = cookie - # Disable them - if config.VERP_PROBES: - log.info('%s: %s disabling due to probe bounce received', - self.internal_name(), member) - else: - log.info('%s: %s disabling due to bounce score %s >= %s', - self.internal_name(), member, - info.score, self.bounce_score_threshold) - self.setDeliveryStatus(member, DeliveryStatus.by_bounces) - self.sendNextNotification(member) - if self.bounce_notify_owner_on_disable: - self.__sendAdminBounceNotice(member, msg) - - def __sendAdminBounceNotice(self, member, msg): - # BAW: This is a bit kludgey, but we're not providing as much - # information in the new admin bounce notices as we used to (some of - # it was of dubious value). However, we'll provide empty, strange, or - # meaningless strings for the unused %()s fields so that the language - # translators don't have to provide new templates. - text = Utils.maketext( - 'bounce.txt', - {'listname' : self.real_name, - 'addr' : member, - 'negative' : '', - 'did' : _('disabled'), - 'but' : '', - 'reenable' : '', - 'owneraddr': self.no_reply_address, - }, mlist=self) - subject = _('Bounce action notification') - umsg = Message.UserNotification(self.GetOwnerEmail(), - self.no_reply_address, - subject, - lang=self.preferred_language) - # BAW: Be sure you set the type before trying to attach, or you'll get - # a MultipartConversionError. - umsg.set_type('multipart/mixed') - umsg.attach( - MIMEText(text, _charset=Utils.GetCharSet(self.preferred_language))) - if isinstance(msg, str): - umsg.attach(MIMEText(msg)) - else: - umsg.attach(MIMEMessage(msg)) - umsg.send(self) - - def sendNextNotification(self, member): - info = self.getBounceInfo(member) - if info is None: - return - reason = self.getDeliveryStatus(member) - if info.noticesleft <= 0: - # BAW: Remove them now, with a notification message - self.ApprovedDeleteMember( - member, 'disabled address', - admin_notif=self.bounce_notify_owner_on_removal, - userack=1) - # Expunge the pending cookie for the user. We throw away the - # returned data. - self.pend_confirm(info.cookie) - if reason == DeliveryStatus.by_bounces: - log.info('%s: %s deleted after exhausting notices', - self.internal_name(), member) - slog.info('%s: %s auto-unsubscribed [reason: %s]', - self.internal_name(), member, - {DeliveryStatus.by_bounces: 'BYBOUNCE', - DeliveryStatus.by_user: 'BYUSER', - DeliveryStatus.by_moderator: 'BYADMIN', - DeliveryStatus.unknown: 'UNKNOWN'}.get( - reason, 'invalid value')) - return - # Send the next notification - confirmurl = '%s/%s' % (self.GetScriptURL('confirm', absolute=1), - info.cookie) - optionsurl = self.GetOptionsURL(member, absolute=1) - reqaddr = self.GetRequestEmail() - lang = self.getMemberLanguage(member) - txtreason = REASONS.get(reason) - if txtreason is None: - txtreason = _('for unknown reasons') - else: - txtreason = _(txtreason) - # Give a little bit more detail on bounce disables - if reason == DeliveryStatus.by_bounces: - date = time.strftime('%d-%b-%Y', - time.localtime(Utils.midnight(info.date))) - extra = _(' The last bounce received from you was dated %(date)s') - txtreason += extra - text = Utils.maketext( - 'disabled.txt', - {'listname' : self.real_name, - 'noticesleft': info.noticesleft, - 'confirmurl' : confirmurl, - 'optionsurl' : optionsurl, - 'password' : self.getMemberPassword(member), - 'owneraddr' : self.GetOwnerEmail(), - 'reason' : txtreason, - }, lang=lang, mlist=self) - msg = Message.UserNotification(member, reqaddr, text=text, lang=lang) - # BAW: See the comment in MailList.py ChangeMemberAddress() for why we - # set the Subject this way. - del msg['subject'] - msg['Subject'] = 'confirm ' + info.cookie - msg.send(self) - info.noticesleft -= 1 - info.lastnotice = time.localtime()[:3] diff --git a/mailman/attic/Defaults.py b/mailman/attic/Defaults.py deleted file mode 100644 index 6f72ed535..000000000 --- a/mailman/attic/Defaults.py +++ /dev/null @@ -1,1324 +0,0 @@ -# Copyright (C) 1998-2009 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 . - -"""Distributed default settings for significant Mailman config variables.""" - -from datetime import timedelta - -from mailman.interfaces.mailinglist import ReplyToMunging - - - -class CompatibleTimeDelta(timedelta): - def __float__(self): - # Convert to float seconds. - return (self.days * 24 * 60 * 60 + - self.seconds + self.microseconds / 1.0e6) - - def __int__(self): - return int(float(self)) - - -def seconds(s): - return CompatibleTimeDelta(seconds=s) - -def minutes(m): - return CompatibleTimeDelta(minutes=m) - -def hours(h): - return CompatibleTimeDelta(hours=h) - -def days(d): - return CompatibleTimeDelta(days=d) - - -# Some convenient constants -Yes = yes = On = on = True -No = no = Off = off = False - - - -##### -# General system-wide defaults -##### - -# Should image logos be used? Set this to 0 to disable image logos from "our -# sponsors" and just use textual links instead (this will also disable the -# shortcut "favicon"). Otherwise, this should contain the URL base path to -# the logo images (and must contain the trailing slash).. If you want to -# disable Mailman's logo footer altogther, hack -# mailman/htmlformat.py:MailmanLogo(), which also contains the hardcoded links -# and image names. -IMAGE_LOGOS = '/icons/' - -# The name of the Mailman favicon -SHORTCUT_ICON = 'mm-icon.png' - -# Don't change MAILMAN_URL, unless you want to point it at one of the mirrors. -MAILMAN_URL = 'http://www.gnu.org/software/mailman/index.html' -#MAILMAN_URL = 'http://www.list.org/' -#MAILMAN_URL = 'http://mailman.sf.net/' - -DEFAULT_URL_PATTERN = 'http://%s/mailman/' - -# This address is used as the from address whenever a message comes from some -# entity to which there is no natural reply recipient. Set this to a real -# human or to /dev/null. It will be appended with the hostname of the list -# involved or the DEFAULT_EMAIL_HOST if none is available. Address must not -# bounce and it must not point to a Mailman process. -NO_REPLY_ADDRESS = 'noreply' - -# This address is the "site owner" address. Certain messages which must be -# delivered to a human, but which can't be delivered to a list owner (e.g. a -# bounce from a list owner), will be sent to this address. It should point to -# a human. -SITE_OWNER_ADDRESS = 'changeme@example.com' - -# Normally when a site administrator authenticates to a web page with the site -# password, they get a cookie which authorizes them as the list admin. It -# makes me nervous to hand out site auth cookies because if this cookie is -# cracked or intercepted, the intruder will have access to every list on the -# site. OTOH, it's dang handy to not have to re-authenticate to every list on -# the site. Set this value to Yes to allow site admin cookies. -ALLOW_SITE_ADMIN_COOKIES = No - -# Command that is used to convert text/html parts into plain text. This -# should output results to standard output. %(filename)s will contain the -# name of the temporary file that the program should operate on. -HTML_TO_PLAIN_TEXT_COMMAND = '/usr/bin/lynx -dump %(filename)s' - -# Default password hashing scheme. See 'bin/mmsitepass -P' for a list of -# available schemes. -PASSWORD_SCHEME = 'ssha' - -# Default run-time directory. -DEFAULT_VAR_DIRECTORY = '/var/mailman' - - - -##### -# Database options -##### - -# Use this to set the SQLAlchemy database engine URL. You generally have one -# primary database connection for all of Mailman. List data and most rosters -# will store their data in this database, although external rosters may access -# other databases in their own way. This string support substitutions using -# any variable in the Configuration object. -DEFAULT_DATABASE_URL = 'sqlite:///$DATA_DIR/mailman.db' - - - -##### -# Spam avoidance defaults -##### - -# This variable contains a list of tuple of the format: -# -# (header, pattern[, chain]) -# -# which is used to match against the current message's headers. If the -# pattern matches the given header in the current message, then the named -# chain is jumped to. header is case-insensitive and should not include the -# trailing colon. pattern is always matched with re.IGNORECASE. chain is -# optional; if not given the 'hold' chain is used, but if given it may be any -# existing chain, such as 'discard', 'reject', or 'accept'. -# -# Note that the more searching done, the slower the whole process gets. -# Header matching is run against all messages coming to either the list, or -# the -owners address, unless the message is explicitly approved. -HEADER_MATCHES = [] - - - -##### -# Web UI defaults -##### - -# Almost all the colors used in Mailman's web interface are parameterized via -# the following variables. This lets you easily change the color schemes for -# your preferences without having to do major surgery on the source code. -# Note that in general, the template colors are not included here since it is -# easy enough to override the default template colors via site-wide, -# vdomain-wide, or list-wide specializations. - -WEB_BG_COLOR = 'white' # Page background -WEB_HEADER_COLOR = '#99ccff' # Major section headers -WEB_SUBHEADER_COLOR = '#fff0d0' # Minor section headers -WEB_ADMINITEM_COLOR = '#dddddd' # Option field background -WEB_ADMINPW_COLOR = '#99cccc' # Password box color -WEB_ERROR_COLOR = 'red' # Error message foreground -WEB_LINK_COLOR = '' # If true, forces LINK= -WEB_ALINK_COLOR = '' # If true, forces ALINK= -WEB_VLINK_COLOR = '' # If true, forces VLINK= -WEB_HIGHLIGHT_COLOR = '#dddddd' # If true, alternating rows - # in listinfo & admin display -# CGI file extension. -CGIEXT = '' - - - -##### -# Archive defaults -##### - -# The url template for the public archives. This will be used in several -# places, including the List-Archive: header, links to the archive on the -# list's listinfo page, and on the list's admin page. -# -# This variable supports several substitution variables -# - $hostname -- the host on which the archive resides -# - $listname -- the short name of the list being accessed -# - $fqdn_listname -- the long name of the list being accessed -PUBLIC_ARCHIVE_URL = 'http://$hostname/pipermail/$fqdn_listname' - -# The public Mail-Archive.com service's base url. -MAIL_ARCHIVE_BASEURL = 'http://go.mail-archive.com/' -# The posting address for the Mail-Archive.com service -MAIL_ARCHIVE_RECIPIENT = 'archive@mail-archive.com' - -# The command for archiving to a local MHonArc instance. -MHONARC_COMMAND = """\ -/usr/bin/mhonarc \ --add \ --dbfile $PRIVATE_ARCHIVE_FILE_DIR/${listname}.mbox/mhonarc.db \ --outdir $VAR_DIR/mhonarc/${listname} \ --stderr $LOG_DIR/mhonarc \ --stdout $LOG_DIR/mhonarc \ --spammode \ --umask 022""" - -# Are archives on or off by default? -DEFAULT_ARCHIVE = On - -# Are archives public or private by default? -# 0=public, 1=private -DEFAULT_ARCHIVE_PRIVATE = 0 - -# ARCHIVE_TO_MBOX -#-1 - do not do any archiving -# 0 - do not archive to mbox, use builtin mailman html archiving only -# 1 - do not use builtin mailman html archiving, archive to mbox only -# 2 - archive to both mbox and builtin mailman html archiving. -# See the settings below for PUBLIC_EXTERNAL_ARCHIVER and -# PRIVATE_EXTERNAL_ARCHIVER which can be used to replace mailman's -# builtin html archiving with an external archiver. The flat mail -# mbox file can be useful for searching, and is another way to -# interface external archivers, etc. -ARCHIVE_TO_MBOX = 2 - -# 0 - yearly -# 1 - monthly -# 2 - quarterly -# 3 - weekly -# 4 - daily -DEFAULT_ARCHIVE_VOLUME_FREQUENCY = 1 -DEFAULT_DIGEST_VOLUME_FREQUENCY = 1 - -# These variables control the use of an external archiver. Normally if -# archiving is turned on (see ARCHIVE_TO_MBOX above and the list's archive* -# attributes) the internal Pipermail archiver is used. This is the default if -# both of these variables are set to No. When either is set, the value should -# be a shell command string which will get passed to os.popen(). This string -# can contain the following substitution strings: -# -# $listname -- gets the internal name of the list -# $hostname -- gets the email hostname for the list -# -# being archived will be substituted for this. Please note that os.popen() is -# used. -# -# Note that if you set one of these variables, you should set both of them -# (they can be the same string). This will mean your external archiver will -# be used regardless of whether public or private archives are selected. -PUBLIC_EXTERNAL_ARCHIVER = No -PRIVATE_EXTERNAL_ARCHIVER = No - -# A filter module that converts from multipart messages to "flat" messages -# (i.e. containing a single payload). This is required for Pipermail, and you -# may want to set it to 0 for external archivers. You can also replace it -# with your own module as long as it contains a process() function that takes -# a MailList object and a Message object. It should raise -# Errors.DiscardMessage if it wants to throw the message away. Otherwise it -# should modify the Message object as necessary. -ARCHIVE_SCRUBBER = 'mailman.pipeline.scrubber' - -# Control parameter whether mailman.Handlers.Scrubber should use message -# attachment's filename as is indicated by the filename parameter or use -# 'attachement-xxx' instead. The default is set True because the applications -# on PC and Mac begin to use longer non-ascii filenames. Historically, it -# was set False in 2.1.6 for backward compatiblity but it was reset to True -# for safer operation in mailman-2.1.7. -SCRUBBER_DONT_USE_ATTACHMENT_FILENAME = True - -# Use of attachment filename extension per se is may be dangerous because -# virus fakes it. You can set this True if you filter the attachment by -# filename extension -SCRUBBER_USE_ATTACHMENT_FILENAME_EXTENSION = False - -# This variable defines what happens to text/html subparts. They can be -# stripped completely, escaped, or filtered through an external program. The -# legal values are: -# 0 - Strip out text/html parts completely, leaving a notice of the removal in -# the message. If the outer part is text/html, the entire message is -# discarded. -# 1 - Remove any embedded text/html parts, leaving them as HTML-escaped -# attachments which can be separately viewed. Outer text/html parts are -# simply HTML-escaped. -# 2 - Leave it inline, but HTML-escape it -# 3 - Remove text/html as attachments but don't HTML-escape them. Note: this -# is very dangerous because it essentially means anybody can send an HTML -# email to your site containing evil JavaScript or web bugs, or other -# nasty things, and folks viewing your archives will be susceptible. You -# should only consider this option if you do heavy moderation of your list -# postings. -# -# Note: given the current archiving code, it is not possible to leave -# text/html parts inline and un-escaped. I wouldn't think it'd be a good idea -# to do anyway. -# -# The value can also be a string, in which case it is the name of a command to -# filter the HTML page through. The resulting output is left in an attachment -# or as the entirety of the message when the outer part is text/html. The -# format of the string must include a "%(filename)s" which will contain the -# name of the temporary file that the program should operate on. It should -# write the processed message to stdout. Set this to -# HTML_TO_PLAIN_TEXT_COMMAND to specify an HTML to plain text conversion -# program. -ARCHIVE_HTML_SANITIZER = 1 - -# Set this to Yes to enable gzipping of the downloadable archive .txt file. -# Note that this is /extremely/ inefficient, so an alternative is to just -# collect the messages in the associated .txt file and run a cron job every -# night to generate the txt.gz file. See cron/nightly_gzip for details. -GZIP_ARCHIVE_TXT_FILES = No - -# This sets the default `clobber date' policy for the archiver. When a -# message is to be archived either by Pipermail or an external archiver, -# Mailman can modify the Date: header to be the date the message was received -# instead of the Date: in the original message. This is useful if you -# typically receive messages with outrageous dates. Set this to 0 to retain -# the date of the original message, or to 1 to always clobber the date. Set -# it to 2 to perform `smart overrides' on the date; when the date is outside -# ARCHIVER_ALLOWABLE_SANE_DATE_SKEW (either too early or too late), then the -# received date is substituted instead. -ARCHIVER_CLOBBER_DATE_POLICY = 2 -ARCHIVER_ALLOWABLE_SANE_DATE_SKEW = days(15) - -# Pipermail archives contain the raw email addresses of the posting authors. -# Some view this as a goldmine for spam harvesters. Set this to Yes to -# moderately obscure email addresses, but note that this breaks mailto: URLs -# in the archives too. -ARCHIVER_OBSCURES_EMAILADDRS = Yes - -# Pipermail assumes that messages bodies contain US-ASCII text. -# Change this option to define a different character set to be used as -# the default character set for the archive. The term "character set" -# is used in MIME to refer to a method of converting a sequence of -# octets into a sequence of characters. If you change the default -# charset, you might need to add it to VERBATIM_ENCODING below. -DEFAULT_CHARSET = None - -# Most character set encodings require special HTML entity characters to be -# quoted, otherwise they won't look right in the Pipermail archives. However -# some character sets must not quote these characters so that they can be -# rendered properly in the browsers. The primary issue is multi-byte -# encodings where the octet 0x26 does not always represent the & character. -# This variable contains a list of such characters sets which are not -# HTML-quoted in the archives. -VERBATIM_ENCODING = ['iso-2022-jp'] - -# When the archive is public, should Mailman also make the raw Unix mbox file -# publically available? -PUBLIC_MBOX = No - - - -##### -# Delivery defaults -##### - -# Final delivery module for outgoing mail. This handler is used for message -# delivery to the list via the smtpd, and to an individual user. This value -# must be a string naming an IHandler. -DELIVERY_MODULE = 'smtp-direct' - -# MTA should name a module in mailman/MTA which provides the MTA specific -# functionality for creating and removing lists. Some MTAs like Exim can be -# configured to automatically recognize new lists, in which case the MTA -# variable should be set to None. Use 'Manual' to print new aliases to -# standard out (or send an email to the site list owner) for manual twiddling -# of an /etc/aliases style file. Use 'Postfix' if you are using the Postfix -# MTA -- but then also see POSTFIX_STYLE_VIRTUAL_DOMAINS. -MTA = 'Manual' - -# If you set MTA='Postfix', then you also want to set the following variable, -# depending on whether you're using virtual domains in Postfix, and which -# style of virtual domain you're using. Set this to the empty list if you're -# not using virtual domains in Postfix, or if you're using Sendmail-style -# virtual domains (where all addresses are visible in all domains). If you're -# using Postfix-style virtual domains, where aliases should only show up in -# the virtual domain, set this variable to the list of host_name values to -# write separate virtual entries for. I.e. if you run dom1.ain, dom2.ain, and -# dom3.ain, but only dom2 and dom3 are virtual, set this variable to the list -# ['dom2.ain', 'dom3.ain']. Matches are done against the host_name attribute -# of the mailing lists. See the Postfix section of the installation manual -# for details. -POSTFIX_STYLE_VIRTUAL_DOMAINS = [] - -# We should use a separator in place of '@' for list-etc@dom2.ain in both -# aliases and mailman-virtual files. -POSTFIX_VIRTUAL_SEPARATOR = '_at_' - -# These variables describe the program to use for regenerating the aliases.db -# and virtual-mailman.db files, respectively, from the associated plain text -# files. The file being updated will be appended to this string (with a -# separating space), so it must be appropriate for os.system(). -POSTFIX_ALIAS_CMD = '/usr/sbin/postalias' -POSTFIX_MAP_CMD = '/usr/sbin/postmap' - -# Ceiling on the number of recipients that can be specified in a single SMTP -# transaction. Set to 0 to submit the entire recipient list in one -# transaction. Only used with the SMTPDirect DELIVERY_MODULE. -SMTP_MAX_RCPTS = 500 - -# Ceiling on the number of SMTP sessions to perform on a single socket -# connection. Some MTAs have limits. Set this to 0 to do as many as we like -# (i.e. your MTA has no limits). Set this to some number great than 0 and -# Mailman will close the SMTP connection and re-open it after this number of -# consecutive sessions. -SMTP_MAX_SESSIONS_PER_CONNECTION = 0 - -# Maximum number of simultaneous subthreads that will be used for SMTP -# delivery. After the recipients list is chunked according to SMTP_MAX_RCPTS, -# each chunk is handed off to the smptd by a separate such thread. If your -# Python interpreter was not built for threads, this feature is disabled. You -# can explicitly disable it in all cases by setting MAX_DELIVERY_THREADS to -# 0. This feature is only supported with the SMTPDirect DELIVERY_MODULE. -# -# NOTE: This is an experimental feature and limited testing shows that it may -# in fact degrade performance, possibly due to Python's global interpreter -# lock. Use with caution. -MAX_DELIVERY_THREADS = 0 - -# SMTP host and port, when DELIVERY_MODULE is 'SMTPDirect'. Make sure the -# host exists and is resolvable (i.e., if it's the default of "localhost" be -# sure there's a localhost entry in your /etc/hosts file!) -SMTPHOST = 'localhost' -SMTPPORT = 0 # default from smtplib - -# Command for direct command pipe delivery to sendmail compatible program, -# when DELIVERY_MODULE is 'Sendmail'. -SENDMAIL_CMD = '/usr/lib/sendmail' - -# Set these variables if you need to authenticate to your NNTP server for -# Usenet posting or reading. If no authentication is necessary, specify None -# for both variables. -NNTP_USERNAME = None -NNTP_PASSWORD = None - -# Set this if you have an NNTP server you prefer gatewayed lists to use. -DEFAULT_NNTP_HOST = u'' - -# These variables controls how headers must be cleansed in order to be -# accepted by your NNTP server. Some servers like INN reject messages -# containing prohibited headers, or duplicate headers. The NNTP server may -# reject the message for other reasons, but there's little that can be -# programmatically done about that. See mailman/Queue/NewsRunner.py -# -# First, these headers (case ignored) are removed from the original message. -NNTP_REMOVE_HEADERS = ['nntp-posting-host', 'nntp-posting-date', 'x-trace', - 'x-complaints-to', 'xref', 'date-received', 'posted', - 'posting-version', 'relay-version', 'received'] - -# Next, these headers are left alone, unless there are duplicates in the -# original message. Any second and subsequent headers are rewritten to the -# second named header (case preserved). -NNTP_REWRITE_DUPLICATE_HEADERS = [ - ('To', 'X-Original-To'), - ('CC', 'X-Original-CC'), - ('Content-Transfer-Encoding', 'X-Original-Content-Transfer-Encoding'), - ('MIME-Version', 'X-MIME-Version'), - ] - -# Some list posts and mail to the -owner address may contain DomainKey or -# DomainKeys Identified Mail (DKIM) signature headers . -# Various list transformations to the message such as adding a list header or -# footer or scrubbing attachments or even reply-to munging can break these -# signatures. It is generally felt that these signatures have value, even if -# broken and even if the outgoing message is resigned. However, some sites -# may wish to remove these headers by setting this to Yes. -REMOVE_DKIM_HEADERS = No - -# This is the pipeline which messages sent to the -owner address go through -OWNER_PIPELINE = [ - 'SpamDetect', - 'Replybot', - 'CleanseDKIM', - 'OwnerRecips', - 'ToOutgoing', - ] - - -# This defines a logging subsystem confirmation file, which overrides the -# default log settings. This is a ConfigParser formatted file which can -# contain sections named after the logger name (without the leading 'mailman.' -# common prefix). Each section may contain the following options: -# -# - level -- Overrides the default level; this may be any of the -# standard Python logging levels, case insensitive. -# - format -- Overrides the default format string; see below. -# - datefmt -- Overrides the default date format string; see below. -# - path -- Overrides the default logger path. This may be a relative -# path name, in which case it is relative to Mailman's LOG_DIR, -# or it may be an absolute path name. You cannot change the -# handler class that will be used. -# - propagate -- Boolean specifying whether to propagate log message from this -# logger to the root "mailman" logger. You cannot override -# settings for the root logger. -# -# The file name may be absolute, or relative to Mailman's etc directory. -LOG_CONFIG_FILE = None - -# This defines log format strings for the SMTPDirect delivery module (see -# DELIVERY_MODULE above). Valid %()s string substitutions include: -# -# time -- the time in float seconds that it took to complete the smtp -# hand-off of the message from Mailman to your smtpd. -# -# size -- the size of the entire message, in bytes -# -# #recips -- the number of actual recipients for this message. -# -# #refused -- the number of smtp refused recipients (use this only in -# SMTP_LOG_REFUSED). -# -# listname -- the `internal' name of the mailing list for this posting -# -# msg_
      -- the value of the delivered message's given header. If -# the message had no such header, then "n/a" will be used. Note though -# that if the message had multiple such headers, then it is undefined -# which will be used. -# -# allmsg_
      - Same as msg_
      above, but if there are multiple -# such headers in the message, they will all be printed, separated by -# comma-space. -# -# sender -- the "sender" of the messages, which will be the From: or -# envelope-sender as determeined by the USE_ENVELOPE_SENDER variable -# below. -# -# The format of the entries is a 2-tuple with the first element naming the -# logger (as a child of the root 'mailman' logger) to print the message to, -# and the second being a format string appropriate for Python's %-style string -# interpolation. The file name is arbitrary; qfiles/ will be created -# automatically if it does not exist. - -# The format of the message printed for every delivered message, regardless of -# whether the delivery was successful or not. Set to None to disable the -# printing of this log message. -SMTP_LOG_EVERY_MESSAGE = ( - 'smtp', - ('${message-id} smtp to $listname for ${#recips} recips, ' - 'completed in $time seconds')) - -# This will only be printed if there were no immediate smtp failures. -# Mutually exclusive with SMTP_LOG_REFUSED. -SMTP_LOG_SUCCESS = ( - 'post', - '${message-id} post to $listname from $sender, size=$size, success') - -# This will only be printed if there were any addresses which encountered an -# immediate smtp failure. Mutually exclusive with SMTP_LOG_SUCCESS. -SMTP_LOG_REFUSED = ( - 'post', - ('${message-id} post to $listname from $sender, size=$size, ' - '${#refused} failures')) - -# This will be logged for each specific recipient failure. Additional %()s -# keys are: -# -# recipient -- the failing recipient address -# failcode -- the smtp failure code -# failmsg -- the actual smtp message, if available -SMTP_LOG_EACH_FAILURE = ( - 'smtp-failure', - ('${message-id} delivery to $recipient failed with code $failcode: ' - '$failmsg')) - -# These variables control the format and frequency of VERP-like delivery for -# better bounce detection. VERP is Variable Envelope Return Path, defined -# here: -# -# http://cr.yp.to/proto/verp.txt -# -# This involves encoding the address of the recipient as we (Mailman) know it -# into the envelope sender address (i.e. the SMTP `MAIL FROM:' address). -# Thus, no matter what kind of forwarding the recipient has in place, should -# it eventually bounce, we will receive an unambiguous notice of the bouncing -# address. -# -# However, we're technically only "VERP-like" because we're doing the envelope -# sender encoding in Mailman, not in the MTA. We do require cooperation from -# the MTA, so you must be sure your MTA can be configured for extended address -# semantics. -# -# The first variable describes how to encode VERP envelopes. It must contain -# these three string interpolations: -# -# %(bounces)s -- the list-bounces mailbox will be set here -# %(mailbox)s -- the recipient's mailbox will be set here -# %(host)s -- the recipient's host name will be set here -# -# This example uses the default below. -# -# FQDN list address is: mylist@dom.ain -# Recipient is: aperson@a.nother.dom -# -# The envelope sender will be mylist-bounces+aperson=a.nother.dom@dom.ain -# -# Note that your MTA /must/ be configured to deliver such an addressed message -# to mylist-bounces! -VERP_DELIMITER = '+' -VERP_FORMAT = '%(bounces)s+%(mailbox)s=%(host)s' - -# The second describes a regular expression to unambiguously decode such an -# address, which will be placed in the To: header of the bounce message by the -# bouncing MTA. Getting this right is critical -- and tricky. Learn your -# Python regular expressions. It must define exactly three named groups, -# bounces, mailbox and host, with the same definition as above. It will be -# compiled case-insensitively. -VERP_REGEXP = r'^(?P[^+]+?)\+(?P[^=]+)=(?P[^@]+)@.*$' - -# VERP format and regexp for probe messages -VERP_PROBE_FORMAT = '%(bounces)s+%(token)s' -VERP_PROBE_REGEXP = r'^(?P[^+]+?)\+(?P[^@]+)@.*$' -# Set this Yes to activate VERP probe for disabling by bounce -VERP_PROBES = No - -# A perfect opportunity for doing VERP is the password reminders, which are -# already addressed individually to each recipient. Set this to Yes to enable -# VERPs on all password reminders. -VERP_PASSWORD_REMINDERS = No - -# Another good opportunity is when regular delivery is personalized. Here -# again, we're already incurring the performance hit for addressing each -# individual recipient. Set this to Yes to enable VERPs on all personalized -# regular deliveries (personalized digests aren't supported yet). -VERP_PERSONALIZED_DELIVERIES = No - -# And finally, we can VERP normal, non-personalized deliveries. However, -# because it can be a significant performance hit, we allow you to decide how -# often to VERP regular deliveries. This is the interval, in number of -# messages, to do a VERP recipient address. The same variable controls both -# regular and digest deliveries. Set to 0 to disable occasional VERPs, set to -# 1 to VERP every delivery, or to some number > 1 for only occasional VERPs. -VERP_DELIVERY_INTERVAL = 0 - -# For nicer confirmation emails, use a VERP-like format which encodes the -# confirmation cookie in the reply address. This lets us put a more user -# friendly Subject: on the message, but requires cooperation from the MTA. -# Format is like VERP_FORMAT above, but with the following substitutions: -# -# $address -- the list-confirm address -# $cookie -- the confirmation cookie -VERP_CONFIRM_FORMAT = '$address+$cookie' - -# This is analogous to VERP_REGEXP, but for splitting apart the -# VERP_CONFIRM_FORMAT. MUAs have been observed that mung -# From: local_part@host -# into -# To: "local_part" -# when replying, so we skip everything up to '<' if any. -VERP_CONFIRM_REGEXP = r'^(.*<)?(?P[^+]+?)\+(?P[^@]+)@.*$' - -# Set this to Yes to enable VERP-like (more user friendly) confirmations -VERP_CONFIRMATIONS = No - -# This is the maximum number of automatic responses sent to an address because -# of -request messages or posting hold messages. This limit prevents response -# loops between Mailman and misconfigured remote email robots. Mailman -# already inhibits automatic replies to any message labeled with a header -# "Precendence: bulk|list|junk". This is a fallback safety valve so it should -# be set fairly high. Set to 0 for no limit (probably useful only for -# debugging). -MAX_AUTORESPONSES_PER_DAY = 10 - - - -##### -# Qrunner defaults -##### - -# Which queues should the qrunner master watchdog spawn? add_qrunner() takes -# one required argument, which is the name of the qrunner to start -# (capitalized and without the 'Runner' suffix). Optional second argument -# specifies the number of parallel processes to fork for each qrunner. If -# more than one process is used, each will take an equal subdivision of the -# hash space, so the number must be a power of 2. -# -# del_qrunners() takes one argument which is the name of the qrunner not to -# start. This is used because by default, Mailman starts the Arch, Bounce, -# Command, Incoming, News, Outgoing, Retry, and Virgin queues. -# -# Set this to Yes to use the `Maildir' delivery option. If you change this -# you will need to re-run bin/genaliases for MTAs that don't use list -# auto-detection. -# -# WARNING: If you want to use Maildir delivery, you /must/ start Mailman's -# qrunner as root, or you will get permission problems. -USE_MAILDIR = No - -# Set this to Yes to use the `LMTP' delivery option. If you change this -# you will need to re-run bin/genaliases for MTAs that don't use list -# auto-detection. -# -# You have to set following line in postfix main.cf: -# transport_maps = hash:/data/transport -# Also needed is following line if your list is in $mydestination: -# alias_maps = hash:/etc/aliases, hash:/data/aliases -USE_LMTP = No - -# Name of the domains which operate on LMTP Mailman only. Currently valid -# only for Postfix alias generation. -LMTP_ONLY_DOMAINS = [] - -# If the list is not present in LMTP_ONLY_DOMAINS, LMTPRunner would return -# 550 response to the master SMTP agent. This may cause 'bounce spam relay' -# in that a spammer expects to deliver the message as bounce info to the -# 'From:' address. You can override this behavior by setting -# LMTP_ERR_550 = '250 Ok. But, blackholed because mailbox unavailable'. -LMTP_ERR_550 = '550 Requested action not taken: mailbox unavailable' - -# WSGI Server. -# -# You must enable PROXY of Apache httpd server and configure to pass Mailman -# CGI requests to this WSGI Server: -# -# ProxyPass /mailman/ http://localhost:2580/mailman/ -# -# Note that local URI part should be the same. -# XXX If you are running Apache 2.2, you will probably also want to set -# ProxyPassReverseCookiePath -# -# Also you have to add following line to /etc/mailman.cfg -# add_qrunner('HTTP') -HTTP_HOST = 'localhost' -HTTP_PORT = 2580 - -# After processing every file in the qrunner's slice, how long should the -# runner sleep for before checking the queue directory again for new files? -# This can be a fraction of a second, or zero to check immediately -# (essentially busy-loop as fast as possible). -QRUNNER_SLEEP_TIME = seconds(1) - -# When a message that is unparsable (by the email package) is received, what -# should we do with it? The most common cause of unparsable messages is -# broken MIME encapsulation, and the most common cause of that is viruses like -# Nimda. Set this variable to No to discard such messages, or to Yes to store -# them in qfiles/bad subdirectory. -QRUNNER_SAVE_BAD_MESSAGES = Yes - -# This flag causes Mailman to fsync() its data files after writing and -# flushing its contents. While this ensures the data is written to disk, -# avoiding data loss, it may be a performance killer. Note that this flag -# affects both message pickles and MailList config.pck files. -SYNC_AFTER_WRITE = No - -# The maximum number of times that the mailmanctl watcher will try to restart -# a qrunner that exits uncleanly. -MAX_RESTARTS = 10 - - - -##### -# General defaults -##### - -# The default language for this server. Whenever we can't figure out the list -# context or user context, we'll fall back to using this language. This code -# must be in the list of available language codes. -DEFAULT_SERVER_LANGUAGE = u'en' - -# When allowing only members to post to a mailing list, how is the sender of -# the message determined? If this variable is set to Yes, then first the -# message's envelope sender is used, with a fallback to the sender if there is -# no envelope sender. Set this variable to No to always use the sender. -# -# The envelope sender is set by the SMTP delivery and is thus less easily -# spoofed than the sender, which is typically just taken from the From: header -# and thus easily spoofed by the end-user. However, sometimes the envelope -# sender isn't set correctly and this will manifest itself by postings being -# held for approval even if they appear to come from a list member. If you -# are having this problem, set this variable to No, but understand that some -# spoofed messages may get through. -USE_ENVELOPE_SENDER = No - -# Membership tests for posting purposes are usually performed by looking at a -# set of headers, passing the test if any of their values match a member of -# the list. Headers are checked in the order given in this variable. The -# value None means use the From_ (envelope sender) header. Field names are -# case insensitive. -SENDER_HEADERS = ('from', None, 'reply-to', 'sender') - -# How many members to display at a time on the admin cgi to unsubscribe them -# or change their options? -DEFAULT_ADMIN_MEMBER_CHUNKSIZE = 30 - -# how many bytes of a held message post should be displayed in the admindb web -# page? Use a negative number to indicate the entire message, regardless of -# size (though this will slow down rendering those pages). -ADMINDB_PAGE_TEXT_LIMIT = 4096 - -# Set this variable to Yes to allow list owners to delete their own mailing -# lists. You may not want to give them this power, in which case, setting -# this variable to No instead requires list removal to be done by the site -# administrator, via the command line script bin/rmlist. -OWNERS_CAN_DELETE_THEIR_OWN_LISTS = No - -# Set this variable to Yes to allow list owners to set the "personalized" -# flags on their mailing lists. Turning these on tells Mailman to send -# separate email messages to each user instead of batching them together for -# delivery to the MTA. This gives each member a more personalized message, -# but can have a heavy impact on the performance of your system. -OWNERS_CAN_ENABLE_PERSONALIZATION = No - -# Should held messages be saved on disk as Python pickles or as plain text? -# The former is more efficient since we don't need to go through the -# parse/generate roundtrip each time, but the latter might be preferred if you -# want to edit the held message on disk. -HOLD_MESSAGES_AS_PICKLES = Yes - -# This variable controls the order in which list-specific category options are -# presented in the admin cgi page. -ADMIN_CATEGORIES = [ - # First column - 'general', 'passwords', 'language', 'members', 'nondigest', 'digest', - # Second column - 'privacy', 'bounce', 'archive', 'gateway', 'autoreply', - 'contentfilter', 'topics', - ] - -# See "Bitfield for user options" below; make this a sum of those options, to -# make all new members of lists start with those options flagged. We assume -# by default that people don't want to receive two copies of posts. Note -# however that the member moderation flag's initial value is controlled by the -# list's config variable default_member_moderation. -DEFAULT_NEW_MEMBER_OPTIONS = 256 - -# Specify the type of passwords to use, when Mailman generates the passwords -# itself, as would be the case for membership requests where the user did not -# fill in a password, or during list creation, when auto-generation of admin -# passwords was selected. -# -# Set this value to Yes for classic Mailman user-friendly(er) passwords. -# These generate semi-pronounceable passwords which are easier to remember. -# Set this value to No to use more cryptographically secure, but harder to -# remember, passwords -- if your operating system and Python version support -# the necessary feature (specifically that /dev/urandom be available). -USER_FRIENDLY_PASSWORDS = Yes -# This value specifies the default lengths of member and list admin passwords -MEMBER_PASSWORD_LENGTH = 8 -ADMIN_PASSWORD_LENGTH = 10 - - - -##### -# List defaults. NOTE: Changing these values does NOT change the -# configuration of an existing list. It only defines the default for new -# lists you subsequently create. -##### - -# Should a list, by default be advertised? What is the default maximum number -# of explicit recipients allowed? What is the default maximum message size -# allowed? -DEFAULT_LIST_ADVERTISED = Yes -DEFAULT_MAX_NUM_RECIPIENTS = 10 -DEFAULT_MAX_MESSAGE_SIZE = 40 # KB - -# These format strings will be expanded w.r.t. the dictionary for the -# mailing list instance. -DEFAULT_SUBJECT_PREFIX = u'[$mlist.real_name] ' -# DEFAULT_SUBJECT_PREFIX = "[$mlist.real_name %%d]" # for numbering -DEFAULT_MSG_HEADER = u'' -DEFAULT_MSG_FOOTER = u"""\ -_______________________________________________ -$real_name mailing list -$fqdn_listname -${listinfo_page} -""" - -# Scrub regular delivery -DEFAULT_SCRUB_NONDIGEST = False - -# Mail command processor will ignore mail command lines after designated max. -EMAIL_COMMANDS_MAX_LINES = 10 - -# Is the list owner notified of admin requests immediately by mail, as well as -# by daily pending-request reminder? -DEFAULT_ADMIN_IMMED_NOTIFY = Yes - -# Is the list owner notified of subscribes/unsubscribes? -DEFAULT_ADMIN_NOTIFY_MCHANGES = No - -# Discard held messages after this days -DEFAULT_MAX_DAYS_TO_HOLD = 0 - -# Should list members, by default, have their posts be moderated? -DEFAULT_DEFAULT_MEMBER_MODERATION = No - -# Should non-member posts which are auto-discarded also be forwarded to the -# moderators? -DEFAULT_FORWARD_AUTO_DISCARDS = Yes - -# What shold happen to non-member posts which are do not match explicit -# non-member actions? -# 0 = Accept -# 1 = Hold -# 2 = Reject -# 3 = Discard -DEFAULT_GENERIC_NONMEMBER_ACTION = 1 - -# Bounce if 'To:', 'Cc:', or 'Resent-To:' fields don't explicitly name list? -# This is an anti-spam measure -DEFAULT_REQUIRE_EXPLICIT_DESTINATION = Yes - -# Alternate names acceptable as explicit destinations for this list. -DEFAULT_ACCEPTABLE_ALIASES = """ -""" -# For mailing lists that have only other mailing lists for members: -DEFAULT_UMBRELLA_LIST = No - -# For umbrella lists, the suffix for the account part of address for -# administrative notices (subscription confirmations, password reminders): -DEFAULT_UMBRELLA_MEMBER_ADMIN_SUFFIX = "-owner" - -# This variable controls whether monthly password reminders are sent. -DEFAULT_SEND_REMINDERS = Yes - -# Send welcome messages to new users? -DEFAULT_SEND_WELCOME_MSG = Yes - -# Send goodbye messages to unsubscribed members? -DEFAULT_SEND_GOODBYE_MSG = Yes - -# Wipe sender information, and make it look like the list-admin -# address sends all messages -DEFAULT_ANONYMOUS_LIST = No - -# {header-name: regexp} spam filtering - we include some for example sake. -DEFAULT_BOUNCE_MATCHING_HEADERS = u""" -# Lines that *start* with a '#' are comments. -to: friend@public.com -message-id: relay.comanche.denmark.eu -from: list@listme.com -from: .*@uplinkpro.com -""" - -# Mailman can be configured to "munge" Reply-To: headers for any passing -# messages. One the one hand, there are a lot of good reasons not to munge -# Reply-To: but on the other, people really seem to want this feature. See -# the help for reply_goes_to_list in the web UI for links discussing the -# issue. -# 0 - Reply-To: not munged -# 1 - Reply-To: set back to the list -# 2 - Reply-To: set to an explicit value (reply_to_address) -DEFAULT_REPLY_GOES_TO_LIST = ReplyToMunging.no_munging - -# Mailman can be configured to strip any existing Reply-To: header, or simply -# extend any existing Reply-To: with one based on the above setting. -DEFAULT_FIRST_STRIP_REPLY_TO = No - -# SUBSCRIBE POLICY -# 0 - open list (only when ALLOW_OPEN_SUBSCRIBE is set to 1) ** -# 1 - confirmation required for subscribes -# 2 - admin approval required for subscribes -# 3 - both confirmation and admin approval required -# -# ** please do not choose option 0 if you are not allowing open -# subscribes (next variable) -DEFAULT_SUBSCRIBE_POLICY = 1 - -# Does this site allow completely unchecked subscriptions? -ALLOW_OPEN_SUBSCRIBE = No - -# This is the default list of addresses and regular expressions (beginning -# with ^) that are exempt from approval if SUBSCRIBE_POLICY is 2 or 3. -DEFAULT_SUBSCRIBE_AUTO_APPROVAL = [] - -# The default policy for unsubscriptions. 0 (unmoderated unsubscribes) is -# highly recommended! -# 0 - unmoderated unsubscribes -# 1 - unsubscribes require approval -DEFAULT_UNSUBSCRIBE_POLICY = 0 - -# Private_roster == 0: anyone can see, 1: members only, 2: admin only. -DEFAULT_PRIVATE_ROSTER = 1 - -# When exposing members, make them unrecognizable as email addrs, so -# web-spiders can't pick up addrs for spam purposes. -DEFAULT_OBSCURE_ADDRESSES = Yes - -# RFC 2369 defines List-* headers which are added to every message sent -# through to the mailing list membership. These are a very useful aid to end -# users and should always be added. However, not all MUAs are compliant and -# if a list's membership has many such users, they may clamor for these -# headers to be suppressed. By setting this variable to Yes, list owners will -# be given the option to suppress these headers. By setting it to No, list -# owners will not be given the option to suppress these headers (although some -# header suppression may still take place, i.e. for announce-only lists, or -# lists with no archives). -ALLOW_RFC2369_OVERRIDES = Yes - -# Defaults for content filtering on mailing lists. DEFAULT_FILTER_CONTENT is -# a flag which if set to true, turns on content filtering. -DEFAULT_FILTER_CONTENT = No - -# DEFAULT_FILTER_MIME_TYPES is a list of MIME types to be removed. This is a -# list of strings of the format "maintype/subtype" or simply "maintype". -# E.g. "text/html" strips all html attachments while "image" strips all image -# types regardless of subtype (jpeg, gif, etc.). -DEFAULT_FILTER_MIME_TYPES = [] - -# DEFAULT_PASS_MIME_TYPES is a list of MIME types to be passed through. -# Format is the same as DEFAULT_FILTER_MIME_TYPES -DEFAULT_PASS_MIME_TYPES = ['multipart/mixed', - 'multipart/alternative', - 'text/plain'] - -# DEFAULT_FILTER_FILENAME_EXTENSIONS is a list of filename extensions to be -# removed. It is useful because many viruses fake their content-type as -# harmless ones while keep their extension as executable and expect to be -# executed when victims 'open' them. -DEFAULT_FILTER_FILENAME_EXTENSIONS = [ - 'exe', 'bat', 'cmd', 'com', 'pif', 'scr', 'vbs', 'cpl' - ] - -# DEFAULT_PASS_FILENAME_EXTENSIONS is a list of filename extensions to be -# passed through. Format is the same as DEFAULT_FILTER_FILENAME_EXTENSIONS. -DEFAULT_PASS_FILENAME_EXTENSIONS = [] - -# Replace multipart/alternative with its first alternative. -DEFAULT_COLLAPSE_ALTERNATIVES = Yes - -# Whether text/html should be converted to text/plain after content filtering -# is performed. Conversion is done according to HTML_TO_PLAIN_TEXT_COMMAND -DEFAULT_CONVERT_HTML_TO_PLAINTEXT = Yes - -# Default action to take on filtered messages. -# 0 = Discard, 1 = Reject, 2 = Forward, 3 = Preserve -DEFAULT_FILTER_ACTION = 0 - -# Whether to allow list owners to preserve content filtered messages to a -# special queue on the disk. -OWNERS_CAN_PRESERVE_FILTERED_MESSAGES = Yes - -# Check for administrivia in messages sent to the main list? -DEFAULT_ADMINISTRIVIA = Yes - - - -##### -# Digestification defaults. Same caveat applies here as with list defaults. -##### - -# Will list be available in non-digested form? -DEFAULT_NONDIGESTABLE = Yes - -# Will list be available in digested form? -DEFAULT_DIGESTABLE = Yes -DEFAULT_DIGEST_HEADER = u'' -DEFAULT_DIGEST_FOOTER = DEFAULT_MSG_FOOTER - -DEFAULT_DIGEST_IS_DEFAULT = No -DEFAULT_MIME_IS_DEFAULT_DIGEST = No -DEFAULT_DIGEST_SIZE_THRESHOLD = 30 # KB -DEFAULT_DIGEST_SEND_PERIODIC = Yes - -# Headers which should be kept in both RFC 1153 (plain) and MIME digests. RFC -# 1153 also specifies these headers in this exact order, so order matters. -MIME_DIGEST_KEEP_HEADERS = [ - 'Date', 'From', 'To', 'Cc', 'Subject', 'Message-ID', 'Keywords', - # I believe we should also keep these headers though. - 'In-Reply-To', 'References', 'Content-Type', 'MIME-Version', - 'Content-Transfer-Encoding', 'Precedence', 'Reply-To', - # Mailman 2.0 adds these headers - 'Message', - ] - -PLAIN_DIGEST_KEEP_HEADERS = [ - 'Message', 'Date', 'From', - 'Subject', 'To', 'Cc', - 'Message-ID', 'Keywords', - 'Content-Type', - ] - - - -##### -# Bounce processing defaults. Same caveat applies here as with list defaults. -##### - -# Should we do any bounced mail response at all? -DEFAULT_BOUNCE_PROCESSING = Yes - -# How often should the bounce qrunner process queued detected bounces? -REGISTER_BOUNCES_EVERY = minutes(15) - -# Bounce processing works like this: when a bounce from a member is received, -# we look up the `bounce info' for this member. If there is no bounce info, -# this is the first bounce we've received from this member. In that case, we -# record today's date, and initialize the bounce score (see below for initial -# value). -# -# If there is existing bounce info for this member, we look at the last bounce -# receive date. If this date is farther away from today than the `bounce -# expiration interval', we throw away all the old data and initialize the -# bounce score as if this were the first bounce from the member. -# -# Otherwise, we increment the bounce score. If we can determine whether the -# bounce was soft or hard (i.e. transient or fatal), then we use a score value -# of 0.5 for soft bounces and 1.0 for hard bounces. Note that we only score -# one bounce per day. If the bounce score is then greater than the `bounce -# threshold' we disable the member's address. -# -# After disabling the address, we can send warning messages to the member, -# providing a confirmation cookie/url for them to use to re-enable their -# delivery. After a configurable period of time, we'll delete the address. -# When we delete the address due to bouncing, we'll send one last message to -# the member. - -# Bounce scores greater than this value get disabled. -DEFAULT_BOUNCE_SCORE_THRESHOLD = 5.0 - -# Bounce information older than this interval is considered stale, and is -# discarded. -DEFAULT_BOUNCE_INFO_STALE_AFTER = days(7) - -# The number of notifications to send to the disabled/removed member before we -# remove them from the list. A value of 0 means we remove the address -# immediately (with one last notification). Note that the first one is sent -# upon change of status to disabled. -DEFAULT_BOUNCE_YOU_ARE_DISABLED_WARNINGS = 3 - -# The interval of time between disabled warnings. -DEFAULT_BOUNCE_YOU_ARE_DISABLED_WARNINGS_INTERVAL = days(7) - -# Does the list owner get messages to the -bounces (and -admin) address that -# failed to match by the bounce detector? -DEFAULT_BOUNCE_UNRECOGNIZED_GOES_TO_LIST_OWNER = Yes - -# Notifications on bounce actions. The first specifies whether the list owner -# should get a notification when a member is disabled due to bouncing, while -# the second specifies whether the owner should get one when the member is -# removed due to bouncing. -DEFAULT_BOUNCE_NOTIFY_OWNER_ON_DISABLE = Yes -DEFAULT_BOUNCE_NOTIFY_OWNER_ON_REMOVAL = Yes - - - -##### -# General time limits -##### - -# Default length of time a pending request is live before it is evicted from -# the pending database. -PENDING_REQUEST_LIFE = days(3) - -# How long should messages which have delivery failures continue to be -# retried? After this period of time, a message that has failed recipients -# will be dequeued and those recipients will never receive the message. -DELIVERY_RETRY_PERIOD = days(5) - -# How long should we wait before we retry a temporary delivery failure? -DELIVERY_RETRY_WAIT = hours(1) - - - -##### -# Lock management defaults -##### - -# These variables control certain aspects of lock acquisition and retention. -# They should be tuned as appropriate for your environment. All variables are -# specified in units of floating point seconds. YOU MAY NEED TO TUNE THESE -# VARIABLES DEPENDING ON THE SIZE OF YOUR LISTS, THE PERFORMANCE OF YOUR -# HARDWARE, NETWORK AND GENERAL MAIL HANDLING CAPABILITIES, ETC. - -# This variable specifies how long the lock will be retained for a specific -# operation on a mailing list. Watch your logs/lock file and if you see a lot -# of lock breakages, you might need to bump this up. However if you set this -# too high, a faulty script (or incorrect use of bin/withlist) can prevent the -# list from being used until the lifetime expires. This is probably one of -# the most crucial tuning variables in the system. -LIST_LOCK_LIFETIME = hours(5) - -# This variable specifies how long an attempt will be made to acquire a list -# lock by the incoming qrunner process. If the lock acquisition times out, -# the message will be re-queued for later delivery. -LIST_LOCK_TIMEOUT = seconds(10) - -# Set this to On to turn on lock debugging messages for the pending requests -# database, which will be written to logs/locks. If you think you're having -# lock problems, or just want to tune the locks for your system, turn on lock -# debugging. -PENDINGDB_LOCK_DEBUGGING = Off - - - -##### -# Nothing below here is user configurable. Most of these values are in this -# file for internal system convenience. Don't change any of them or override -# any of them in your mailman.cfg file! -##### - -# Enumeration for Mailman cgi widget types -Toggle = 1 -Radio = 2 -String = 3 -Text = 4 -Email = 5 -EmailList = 6 -Host = 7 -Number = 8 -FileUpload = 9 -Select = 10 -Topics = 11 -Checkbox = 12 -# An "extended email list". Contents must be an email address or a ^-prefixed -# regular expression. Used in the sender moderation text boxes. -EmailListEx = 13 -# Extended spam filter widget -HeaderFilter = 14 - -# Actions -DEFER = 0 -APPROVE = 1 -REJECT = 2 -DISCARD = 3 -SUBSCRIBE = 4 -UNSUBSCRIBE = 5 -ACCEPT = 6 -HOLD = 7 - -# Standard text field width -TEXTFIELDWIDTH = 40 - -# Bitfield for user options. See DEFAULT_NEW_MEMBER_OPTIONS above to set -# defaults for all new lists. -Digests = 0 # handled by other mechanism, doesn't need a flag. -DisableDelivery = 1 # Obsolete; use set/getDeliveryStatus() -DontReceiveOwnPosts = 2 # Non-digesters only -AcknowledgePosts = 4 -DisableMime = 8 # Digesters only -ConcealSubscription = 16 -SuppressPasswordReminder = 32 -ReceiveNonmatchingTopics = 64 -Moderate = 128 -DontReceiveDuplicates = 256 - - -# A mapping between short option tags and their flag -OPTINFO = {'hide' : ConcealSubscription, - 'nomail' : DisableDelivery, - 'ack' : AcknowledgePosts, - 'notmetoo': DontReceiveOwnPosts, - 'digest' : 0, - 'plain' : DisableMime, - 'nodupes' : DontReceiveDuplicates - } - -# Authentication contexts. -# -# Mailman defines the following roles: - -# - User, a normal user who has no permissions except to change their personal -# option settings -# - List creator, someone who can create and delete lists, but cannot -# (necessarily) configure the list. -# - List moderator, someone who can tend to pending requests such as -# subscription requests, or held messages -# - List administrator, someone who has total control over a list, can -# configure it, modify user options for members of the list, subscribe and -# unsubscribe members, etc. -# - Site administrator, someone who has total control over the entire site and -# can do any of the tasks mentioned above. This person usually also has -# command line access. - -UnAuthorized = 0 -AuthUser = 1 # Joe Shmoe User -AuthCreator = 2 # List Creator / Destroyer -AuthListAdmin = 3 # List Administrator (total control over list) -AuthListModerator = 4 # List Moderator (can only handle held requests) -AuthSiteAdmin = 5 # Site Administrator (total control over everything) - - - -# Vgg: Language descriptions and charsets dictionary, any new supported -# language must have a corresponding entry here. Key is the name of the -# directories that hold the localized texts. Data are tuples with first -# element being the description, as described in the catalogs, and second -# element is the language charset. I have chosen code from /usr/share/locale -# in my GNU/Linux. :-) -# -# TK: Now the site admin can select languages for the installation from those -# in the distribution tarball. We don't touch add_language() function for -# backward compatibility. You may have to add your own language in your -# mailman.cfg file, if it is not included in the distribution even if you had -# put language files in source directory and configured by `--with-languages' -# option. -def _(s): - return s - -_DEFAULT_LANGUAGE_DATA = { - 'ar': (_('Arabic'), 'utf-8'), - 'ca': (_('Catalan'), 'iso-8859-1'), - 'cs': (_('Czech'), 'iso-8859-2'), - 'da': (_('Danish'), 'iso-8859-1'), - 'de': (_('German'), 'iso-8859-1'), - 'en': (_('English (USA)'), 'us-ascii'), - 'es': (_('Spanish (Spain)'), 'iso-8859-1'), - 'et': (_('Estonian'), 'iso-8859-15'), - 'eu': (_('Euskara'), 'iso-8859-15'), # Basque - 'fi': (_('Finnish'), 'iso-8859-1'), - 'fr': (_('French'), 'iso-8859-1'), - 'hr': (_('Croatian'), 'iso-8859-2'), - 'hu': (_('Hungarian'), 'iso-8859-2'), - 'ia': (_('Interlingua'), 'iso-8859-15'), - 'it': (_('Italian'), 'iso-8859-1'), - 'ja': (_('Japanese'), 'euc-jp'), - 'ko': (_('Korean'), 'euc-kr'), - 'lt': (_('Lithuanian'), 'iso-8859-13'), - 'nl': (_('Dutch'), 'iso-8859-1'), - 'no': (_('Norwegian'), 'iso-8859-1'), - 'pl': (_('Polish'), 'iso-8859-2'), - 'pt': (_('Portuguese'), 'iso-8859-1'), - 'pt_BR': (_('Portuguese (Brazil)'), 'iso-8859-1'), - 'ro': (_('Romanian'), 'iso-8859-2'), - 'ru': (_('Russian'), 'koi8-r'), - 'sr': (_('Serbian'), 'utf-8'), - 'sl': (_('Slovenian'), 'iso-8859-2'), - 'sv': (_('Swedish'), 'iso-8859-1'), - 'tr': (_('Turkish'), 'iso-8859-9'), - 'uk': (_('Ukrainian'), 'utf-8'), - 'vi': (_('Vietnamese'), 'utf-8'), - 'zh_CN': (_('Chinese (China)'), 'utf-8'), - 'zh_TW': (_('Chinese (Taiwan)'), 'utf-8'), -} - - -del _ diff --git a/mailman/attic/Deliverer.py b/mailman/attic/Deliverer.py deleted file mode 100644 index 0ba3a01bb..000000000 --- a/mailman/attic/Deliverer.py +++ /dev/null @@ -1,174 +0,0 @@ -# Copyright (C) 1998-2009 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 . - - -"""Mixin class with message delivery routines.""" - -from __future__ import with_statement - -import logging - -from email.MIMEMessage import MIMEMessage -from email.MIMEText import MIMEText - -from mailman import Errors -from mailman import Message -from mailman import Utils -from mailman import i18n -from mailman.configuration import config - -_ = i18n._ - -log = logging.getLogger('mailman.error') -mlog = logging.getLogger('mailman.mischief') - - - -class Deliverer: - def MailUserPassword(self, user): - listfullname = self.fqdn_listname - requestaddr = self.GetRequestEmail() - # find the lowercased version of the user's address - adminaddr = self.GetBouncesEmail() - assert self.isMember(user) - if not self.getMemberPassword(user): - # The user's password somehow got corrupted. Generate a new one - # for him, after logging this bogosity. - log.error('User %s had a false password for list %s', - user, self.internal_name()) - waslocked = self.Locked() - if not waslocked: - self.Lock() - try: - self.setMemberPassword(user, Utils.MakeRandomPassword()) - self.Save() - finally: - if not waslocked: - self.Unlock() - # Now send the user his password - cpuser = self.getMemberCPAddress(user) - recipient = self.GetMemberAdminEmail(cpuser) - subject = _('%(listfullname)s mailing list reminder') - # Get user's language and charset - lang = self.getMemberLanguage(user) - cset = Utils.GetCharSet(lang) - password = self.getMemberPassword(user) - # TK: Make unprintables to ? - # The list owner should allow users to set language options if they - # want to use non-us-ascii characters in password and send it back. - password = unicode(password, cset, 'replace').encode(cset, 'replace') - # get the text from the template - text = Utils.maketext( - 'userpass.txt', - {'user' : cpuser, - 'listname' : self.real_name, - 'fqdn_lname' : self.GetListEmail(), - 'password' : password, - 'options_url': self.GetOptionsURL(user, absolute=True), - 'requestaddr': requestaddr, - 'owneraddr' : self.GetOwnerEmail(), - }, lang=lang, mlist=self) - msg = Message.UserNotification(recipient, adminaddr, subject, text, - lang) - msg['X-No-Archive'] = 'yes' - msg.send(self, verp=config.VERP_PERSONALIZED_DELIVERIES) - - def ForwardMessage(self, msg, text=None, subject=None, tomoderators=True): - # Wrap the message as an attachment - if text is None: - text = _('No reason given') - if subject is None: - text = _('(no subject)') - text = MIMEText(Utils.wrap(text), - _charset=Utils.GetCharSet(self.preferred_language)) - attachment = MIMEMessage(msg) - notice = Message.OwnerNotification( - self, subject, tomoderators=tomoderators) - # Make it look like the message is going to the -owner address - notice.set_type('multipart/mixed') - notice.attach(text) - notice.attach(attachment) - notice.send(self) - - def SendHostileSubscriptionNotice(self, listname, address): - # Some one was invited to one list but tried to confirm to a different - # list. We inform both list owners of the bogosity, but be careful - # not to reveal too much information. - selfname = self.internal_name() - mlog.error('%s was invited to %s but confirmed to %s', - address, listname, selfname) - # First send a notice to the attacked list - msg = Message.OwnerNotification( - self, - _('Hostile subscription attempt detected'), - Utils.wrap(_("""%(address)s was invited to a different mailing -list, but in a deliberate malicious attempt they tried to confirm the -invitation to your list. We just thought you'd like to know. No further -action by you is required."""))) - msg.send(self) - # Now send a notice to the invitee list - try: - # Avoid import loops - from mailman.MailList import MailList - mlist = MailList(listname, lock=False) - except Errors.MMListError: - # Oh well - return - with i18n.using_language(mlist.preferred_language): - msg = Message.OwnerNotification( - mlist, - _('Hostile subscription attempt detected'), - Utils.wrap(_("""You invited %(address)s to your list, but in a -deliberate malicious attempt, they tried to confirm the invitation to a -different list. We just thought you'd like to know. No further action by you -is required."""))) - msg.send(mlist) - - def sendProbe(self, member, msg): - listname = self.real_name - # Put together the substitution dictionary. - d = {'listname': listname, - 'address': member, - 'optionsurl': self.GetOptionsURL(member, absolute=True), - 'owneraddr': self.GetOwnerEmail(), - } - text = Utils.maketext('probe.txt', d, - lang=self.getMemberLanguage(member), - mlist=self) - # Calculate the VERP'd sender address for bounce processing of the - # probe message. - token = self.pend_new(Pending.PROBE_BOUNCE, member, msg) - probedict = { - 'bounces': self.internal_name() + '-bounces', - 'token': token, - } - probeaddr = '%s@%s' % ((config.VERP_PROBE_FORMAT % probedict), - self.host_name) - # Calculate the Subject header, in the member's preferred language - ulang = self.getMemberLanguage(member) - with i18n.using_language(ulang): - subject = _('%(listname)s mailing list probe message') - outer = Message.UserNotification(member, probeaddr, subject, - lang=ulang) - outer.set_type('multipart/mixed') - text = MIMEText(text, _charset=Utils.GetCharSet(ulang)) - outer.attach(text) - outer.attach(MIMEMessage(msg)) - # Turn off further VERP'ing in the final delivery step. We set - # probe_token for the OutgoingRunner to more easily handling local - # rejects of probe messages. - outer.send(self, envsender=probeaddr, verp=False, probe_token=token) diff --git a/mailman/attic/Digester.py b/mailman/attic/Digester.py deleted file mode 100644 index a88d08abc..000000000 --- a/mailman/attic/Digester.py +++ /dev/null @@ -1,57 +0,0 @@ -# Copyright (C) 1998-2009 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 . - - -"""Mixin class with list-digest handling methods and settings.""" - -import os -import errno - -from mailman import Errors -from mailman import Utils -from mailman.Handlers import ToDigest -from mailman.configuration import config -from mailman.i18n import _ - - - -class Digester: - def send_digest_now(self): - # Note: Handler.ToDigest.send_digests() handles bumping the digest - # volume and issue number. - digestmbox = os.path.join(self.fullpath(), 'digest.mbox') - try: - try: - mboxfp = None - # See if there's a digest pending for this mailing list - if os.stat(digestmbox).st_size > 0: - mboxfp = open(digestmbox) - ToDigest.send_digests(self, mboxfp) - os.unlink(digestmbox) - finally: - if mboxfp: - mboxfp.close() - except OSError, e: - if e.errno <> errno.ENOENT: - raise - # List has no outstanding digests - return False - return True - - def bump_digest_volume(self): - self.volume += 1 - self.next_digest_number = 1 diff --git a/mailman/attic/MailList.py b/mailman/attic/MailList.py deleted file mode 100644 index 2d538f026..000000000 --- a/mailman/attic/MailList.py +++ /dev/null @@ -1,731 +0,0 @@ -# Copyright (C) 1998-2009 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 . - - -"""The class representing a Mailman mailing list. - -Mixes in many task-specific classes. -""" - -from __future__ import with_statement - -import os -import re -import sys -import time -import errno -import shutil -import socket -import urllib -import cPickle -import logging -import marshal -import email.Iterators - -from UserDict import UserDict -from cStringIO import StringIO -from string import Template -from types import MethodType -from urlparse import urlparse -from zope.interface import implements - -from email.Header import Header -from email.Utils import getaddresses, formataddr, parseaddr - -from Mailman import Errors -from Mailman import Utils -from Mailman import Version -from Mailman import database -from Mailman.UserDesc import UserDesc -from Mailman.configuration import config -from Mailman.interfaces import * - -# Base classes -from Mailman.Archiver import Archiver -from Mailman.Bouncer import Bouncer -from Mailman.Digester import Digester -from Mailman.SecurityManager import SecurityManager - -# GUI components package -from Mailman import Gui - -# Other useful classes -from Mailman import i18n -from Mailman import MemberAdaptor -from Mailman import Message - -_ = i18n._ - -DOT = '.' -EMPTYSTRING = '' -OR = '|' - -clog = logging.getLogger('mailman.config') -elog = logging.getLogger('mailman.error') -vlog = logging.getLogger('mailman.vette') -slog = logging.getLogger('mailman.subscribe') - - - -# Use mixins here just to avoid having any one chunk be too large. -class MailList(object, Archiver, Digester, SecurityManager, Bouncer): - - implements( - IMailingList, - IMailingListAddresses, - IMailingListIdentity, - IMailingListRosters, - ) - - def __init__(self, data): - self._data = data - # Only one level of mixin inheritance allowed. - for baseclass in self.__class__.__bases__: - if hasattr(baseclass, '__init__'): - baseclass.__init__(self) - # Initialize the web u/i components. - self._gui = [] - for component in dir(Gui): - if component.startswith('_'): - continue - self._gui.append(getattr(Gui, component)()) - # Give the extension mechanism a chance to process this list. - try: - from Mailman.ext import init_mlist - except ImportError: - pass - else: - init_mlist(self) - - def __getattr__(self, name): - missing = object() - if name.startswith('_'): - return getattr(super(MailList, self), name) - # Delegate to the database model object if it has the attribute. - obj = getattr(self._data, name, missing) - if obj is not missing: - return obj - # Finally, delegate to one of the gui components. - for guicomponent in self._gui: - obj = getattr(guicomponent, name, missing) - if obj is not missing: - return obj - # Nothing left to delegate to, so it's got to be an error. - raise AttributeError(name) - - def __repr__(self): - return '' % (self.fqdn_listname, id(self)) - - - def GetConfirmJoinSubject(self, listname, cookie): - if config.VERP_CONFIRMATIONS and cookie: - cset = i18n.get_translation().charset() or \ - Utils.GetCharSet(self.preferred_language) - subj = Header( - _('Your confirmation is required to join the %(listname)s mailing list'), - cset, header_name='subject') - return subj - else: - return 'confirm ' + cookie - - def GetConfirmLeaveSubject(self, listname, cookie): - if config.VERP_CONFIRMATIONS and cookie: - cset = i18n.get_translation().charset() or \ - Utils.GetCharSet(self.preferred_language) - subj = Header( - _('Your confirmation is required to leave the %(listname)s mailing list'), - cset, header_name='subject') - return subj - else: - return 'confirm ' + cookie - - def GetMemberAdminEmail(self, member): - """Usually the member addr, but modified for umbrella lists. - - Umbrella lists have other mailing lists as members, and so admin stuff - like confirmation requests and passwords must not be sent to the - member addresses - the sublists - but rather to the administrators of - the sublists. This routine picks the right address, considering - regular member address to be their own administrative addresses. - - """ - if not self.umbrella_list: - return member - else: - acct, host = tuple(member.split('@')) - return "%s%s@%s" % (acct, self.umbrella_member_suffix, host) - - def GetScriptURL(self, target, absolute=False): - if absolute: - return self.web_page_url + target + '/' + self.fqdn_listname - else: - return Utils.ScriptURL(target) + '/' + self.fqdn_listname - - def GetOptionsURL(self, user, obscure=False, absolute=False): - url = self.GetScriptURL('options', absolute) - if obscure: - user = Utils.ObscureEmail(user) - return '%s/%s' % (url, urllib.quote(user.lower())) - - - # - # Web API support via administrative categories - # - def GetConfigCategories(self): - class CategoryDict(UserDict): - def __init__(self): - UserDict.__init__(self) - self.keysinorder = config.ADMIN_CATEGORIES[:] - def keys(self): - return self.keysinorder - def items(self): - items = [] - for k in config.ADMIN_CATEGORIES: - items.append((k, self.data[k])) - return items - def values(self): - values = [] - for k in config.ADMIN_CATEGORIES: - values.append(self.data[k]) - return values - - categories = CategoryDict() - # Only one level of mixin inheritance allowed - for gui in self._gui: - k, v = gui.GetConfigCategory() - categories[k] = (v, gui) - return categories - - def GetConfigSubCategories(self, category): - for gui in self._gui: - if hasattr(gui, 'GetConfigSubCategories'): - # Return the first one that knows about the given subcategory - subcat = gui.GetConfigSubCategories(category) - if subcat is not None: - return subcat - return None - - def GetConfigInfo(self, category, subcat=None): - for gui in self._gui: - if hasattr(gui, 'GetConfigInfo'): - value = gui.GetConfigInfo(self, category, subcat) - if value: - return value - - - # - # Membership management front-ends and assertion checks - # - def InviteNewMember(self, userdesc, text=''): - """Invite a new member to the list. - - This is done by creating a subscription pending for the user, and then - crafting a message to the member informing them of the invitation. - """ - invitee = userdesc.address - Utils.ValidateEmail(invitee) - # check for banned address - pattern = Utils.get_pattern(invitee, self.ban_list) - if pattern: - raise Errors.MembershipIsBanned(pattern) - # Hack alert! Squirrel away a flag that only invitations have, so - # that we can do something slightly different when an invitation - # subscription is confirmed. In those cases, we don't need further - # admin approval, even if the list is so configured. The flag is the - # list name to prevent invitees from cross-subscribing. - userdesc.invitation = self.internal_name() - cookie = self.pend_new(Pending.SUBSCRIPTION, userdesc) - requestaddr = self.getListAddress('request') - confirmurl = '%s/%s' % (self.GetScriptURL('confirm', absolute=1), - cookie) - listname = self.real_name - text += Utils.maketext( - 'invite.txt', - {'email' : invitee, - 'listname' : listname, - 'hostname' : self.host_name, - 'confirmurl' : confirmurl, - 'requestaddr': requestaddr, - 'cookie' : cookie, - 'listowner' : self.GetOwnerEmail(), - }, mlist=self) - sender = self.GetRequestEmail(cookie) - msg = Message.UserNotification( - invitee, sender, - text=text, lang=self.preferred_language) - subj = self.GetConfirmJoinSubject(listname, cookie) - del msg['subject'] - msg['Subject'] = subj - msg.send(self) - - def AddMember(self, userdesc, remote=None): - """Front end to member subscription. - - This method enforces subscription policy, validates values, sends - notifications, and any other grunt work involved in subscribing a - user. It eventually calls ApprovedAddMember() to do the actual work - of subscribing the user. - - userdesc is an instance with the following public attributes: - - address -- the unvalidated email address of the member - fullname -- the member's full name (i.e. John Smith) - digest -- a flag indicating whether the user wants digests or not - language -- the requested default language for the user - password -- the user's password - - Other attributes may be defined later. Only address is required; the - others all have defaults (fullname='', digests=0, language=list's - preferred language, password=generated). - - remote is a string which describes where this add request came from. - """ - assert self.Locked() - # Suck values out of userdesc, apply defaults, and reset the userdesc - # attributes (for passing on to ApprovedAddMember()). Lowercase the - # addr's domain part. - email = Utils.LCDomain(userdesc.address) - name = getattr(userdesc, 'fullname', '') - lang = getattr(userdesc, 'language', self.preferred_language) - digest = getattr(userdesc, 'digest', None) - password = getattr(userdesc, 'password', Utils.MakeRandomPassword()) - if digest is None: - if self.nondigestable: - digest = 0 - else: - digest = 1 - # Validate the e-mail address to some degree. - Utils.ValidateEmail(email) - if self.isMember(email): - raise Errors.MMAlreadyAMember, email - if email.lower() == self.GetListEmail().lower(): - # Trying to subscribe the list to itself! - raise Errors.InvalidEmailAddress - realname = self.real_name - # Is the subscribing address banned from this list? - pattern = Utils.get_pattern(email, self.ban_list) - if pattern: - vlog.error('%s banned subscription: %s (matched: %s)', - realname, email, pattern) - raise Errors.MembershipIsBanned, pattern - # Sanity check the digest flag - if digest and not self.digestable: - raise Errors.MMCantDigestError - elif not digest and not self.nondigestable: - raise Errors.MMMustDigestError - - userdesc.address = email - userdesc.fullname = name - userdesc.digest = digest - userdesc.language = lang - userdesc.password = password - - # Apply the list's subscription policy. 0 means open subscriptions; 1 - # means the user must confirm; 2 means the admin must approve; 3 means - # the user must confirm and then the admin must approve - if self.subscribe_policy == 0: - self.ApprovedAddMember(userdesc, whence=remote or '') - elif self.subscribe_policy == 1 or self.subscribe_policy == 3: - # User confirmation required. BAW: this should probably just - # accept a userdesc instance. - cookie = self.pend_new(Pending.SUBSCRIPTION, userdesc) - # Send the user the confirmation mailback - if remote is None: - by = remote = '' - else: - by = ' ' + remote - remote = _(' from %(remote)s') - - recipient = self.GetMemberAdminEmail(email) - confirmurl = '%s/%s' % (self.GetScriptURL('confirm', absolute=1), - cookie) - text = Utils.maketext( - 'verify.txt', - {'email' : email, - 'listaddr' : self.GetListEmail(), - 'listname' : realname, - 'cookie' : cookie, - 'requestaddr' : self.getListAddress('request'), - 'remote' : remote, - 'listadmin' : self.GetOwnerEmail(), - 'confirmurl' : confirmurl, - }, lang=lang, mlist=self) - msg = Message.UserNotification( - recipient, self.GetRequestEmail(cookie), - text=text, lang=lang) - # BAW: See ChangeMemberAddress() for why we do it this way... - del msg['subject'] - msg['Subject'] = self.GetConfirmJoinSubject(realname, cookie) - msg['Reply-To'] = self.GetRequestEmail(cookie) - msg.send(self) - who = formataddr((name, email)) - slog.info('%s: pending %s %s', self.internal_name(), who, by) - raise Errors.MMSubscribeNeedsConfirmation - elif self.HasAutoApprovedSender(email): - # no approval necessary: - self.ApprovedAddMember(userdesc) - else: - # Subscription approval is required. Add this entry to the admin - # requests database. BAW: this should probably take a userdesc - # just like above. - self.HoldSubscription(email, name, password, digest, lang) - raise Errors.MMNeedApproval, _( - 'subscriptions to %(realname)s require moderator approval') - - def DeleteMember(self, name, whence=None, admin_notif=None, userack=True): - realname, email = parseaddr(name) - if self.unsubscribe_policy == 0: - self.ApprovedDeleteMember(name, whence, admin_notif, userack) - else: - self.HoldUnsubscription(email) - raise Errors.MMNeedApproval, _( - 'unsubscriptions require moderator approval') - - def ChangeMemberAddress(self, oldaddr, newaddr, globally): - # Changing a member address consists of verifying the new address, - # making sure the new address isn't already a member, and optionally - # going through the confirmation process. - # - # Most of these checks are copied from AddMember - newaddr = Utils.LCDomain(newaddr) - Utils.ValidateEmail(newaddr) - # Raise an exception if this email address is already a member of the - # list, but only if the new address is the same case-wise as the old - # address and we're not doing a global change. - if not globally and newaddr == oldaddr and self.isMember(newaddr): - raise Errors.MMAlreadyAMember - if newaddr == self.GetListEmail().lower(): - raise Errors.InvalidEmailAddress - realname = self.real_name - # Don't allow changing to a banned address. MAS: maybe we should - # unsubscribe the oldaddr too just for trying, but that's probably - # too harsh. - pattern = Utils.get_pattern(newaddr, self.ban_list) - if pattern: - vlog.error('%s banned address change: %s -> %s (matched: %s)', - realname, oldaddr, newaddr, pattern) - raise Errors.MembershipIsBanned, pattern - # Pend the subscription change - cookie = self.pend_new(Pending.CHANGE_OF_ADDRESS, - oldaddr, newaddr, globally) - confirmurl = '%s/%s' % (self.GetScriptURL('confirm', absolute=1), - cookie) - lang = self.getMemberLanguage(oldaddr) - text = Utils.maketext( - 'verify.txt', - {'email' : newaddr, - 'listaddr' : self.GetListEmail(), - 'listname' : realname, - 'cookie' : cookie, - 'requestaddr': self.getListAddress('request'), - 'remote' : '', - 'listadmin' : self.GetOwnerEmail(), - 'confirmurl' : confirmurl, - }, lang=lang, mlist=self) - # BAW: We don't pass the Subject: into the UserNotification - # constructor because it will encode it in the charset of the language - # being used. For non-us-ascii charsets, this means it will probably - # quopri quote it, and thus replies will also be quopri encoded. But - # CommandRunner doesn't yet grok such headers. So, just set the - # Subject: in a separate step, although we have to delete the one - # UserNotification adds. - msg = Message.UserNotification( - newaddr, self.GetRequestEmail(cookie), - text=text, lang=lang) - del msg['subject'] - msg['Subject'] = self.GetConfirmJoinSubject(realname, cookie) - msg['Reply-To'] = self.GetRequestEmail(cookie) - msg.send(self) - - def ApprovedChangeMemberAddress(self, oldaddr, newaddr, globally): - # Check here for banned address in case address was banned after - # confirmation was mailed. MAS: If it's global change should we just - # skip this list and proceed to the others? For now we'll throw the - # exception. - pattern = Utils.get_pattern(newaddr, self.ban_list) - if pattern: - raise Errors.MembershipIsBanned, pattern - # It's possible they were a member of this list, but choose to change - # their membership globally. In that case, we simply remove the old - # address. - if self.getMemberCPAddress(oldaddr) == newaddr: - self.removeMember(oldaddr) - else: - self.changeMemberAddress(oldaddr, newaddr) - self.log_and_notify_admin(oldaddr, newaddr) - # If globally is true, then we also include every list for which - # oldaddr is a member. - if not globally: - return - for listname in config.list_manager.names: - # Don't bother with ourselves - if listname == self.internal_name(): - continue - mlist = MailList(listname, lock=0) - if mlist.host_name <> self.host_name: - continue - if not mlist.isMember(oldaddr): - continue - # If new address is banned from this list, just skip it. - if Utils.get_pattern(newaddr, mlist.ban_list): - continue - mlist.Lock() - try: - # Same logic as above, re newaddr is already a member - if mlist.getMemberCPAddress(oldaddr) == newaddr: - mlist.removeMember(oldaddr) - else: - mlist.changeMemberAddress(oldaddr, newaddr) - mlist.log_and_notify_admin(oldaddr, newaddr) - mlist.Save() - finally: - mlist.Unlock() - - def log_and_notify_admin(self, oldaddr, newaddr): - """Log member address change and notify admin if requested.""" - slog.info('%s: changed member address from %s to %s', - self.internal_name(), oldaddr, newaddr) - if self.admin_notify_mchanges: - with i18n.using_language(self.preferred_language): - realname = self.real_name - subject = _('%(realname)s address change notification') - name = self.getMemberName(newaddr) - if name is None: - name = '' - if isinstance(name, unicode): - name = name.encode(Utils.GetCharSet(self.preferred_language), - 'replace') - text = Utils.maketext( - 'adminaddrchgack.txt', - {'name' : name, - 'oldaddr' : oldaddr, - 'newaddr' : newaddr, - 'listname': self.real_name, - }, mlist=self) - msg = Message.OwnerNotification(self, subject, text) - msg.send(self) - - - # - # Confirmation processing - # - def ProcessConfirmation(self, cookie, context=None): - rec = self.pend_confirm(cookie) - if rec is None: - raise Errors.MMBadConfirmation, 'No cookie record for %s' % cookie - try: - op = rec[0] - data = rec[1:] - except ValueError: - raise Errors.MMBadConfirmation, 'op-less data %s' % (rec,) - if op == Pending.SUBSCRIPTION: - whence = 'via email confirmation' - try: - userdesc = data[0] - # If confirmation comes from the web, context should be a - # UserDesc instance which contains overrides of the original - # subscription information. If it comes from email, then - # context is a Message and isn't relevant, so ignore it. - if isinstance(context, UserDesc): - userdesc += context - whence = 'via web confirmation' - addr = userdesc.address - fullname = userdesc.fullname - password = userdesc.password - digest = userdesc.digest - lang = userdesc.language - except ValueError: - raise Errors.MMBadConfirmation, 'bad subscr data %s' % (data,) - # Hack alert! Was this a confirmation of an invitation? - invitation = getattr(userdesc, 'invitation', False) - # We check for both 2 (approval required) and 3 (confirm + - # approval) because the policy could have been changed in the - # middle of the confirmation dance. - if invitation: - if invitation <> self.internal_name(): - # Not cool. The invitee was trying to subscribe to a - # different list than they were invited to. Alert both - # list administrators. - self.SendHostileSubscriptionNotice(invitation, addr) - raise Errors.HostileSubscriptionError - elif self.subscribe_policy in (2, 3) and \ - not self.HasAutoApprovedSender(addr): - self.HoldSubscription(addr, fullname, password, digest, lang) - name = self.real_name - raise Errors.MMNeedApproval, _( - 'subscriptions to %(name)s require administrator approval') - self.ApprovedAddMember(userdesc, whence=whence) - return op, addr, password, digest, lang - elif op == Pending.UNSUBSCRIPTION: - addr = data[0] - # Log file messages don't need to be i18n'd - if isinstance(context, Message.Message): - whence = 'email confirmation' - else: - whence = 'web confirmation' - # Can raise NotAMemberError if they unsub'd via other means - self.ApprovedDeleteMember(addr, whence=whence) - return op, addr - elif op == Pending.CHANGE_OF_ADDRESS: - oldaddr, newaddr, globally = data - self.ApprovedChangeMemberAddress(oldaddr, newaddr, globally) - return op, oldaddr, newaddr - elif op == Pending.HELD_MESSAGE: - id = data[0] - approved = None - # Confirmation should be coming from email, where context should - # be the confirming message. If the message does not have an - # Approved: header, this is a discard. If it has an Approved: - # header that does not match the list password, then we'll notify - # the list administrator that they used the wrong password. - # Otherwise it's an approval. - if isinstance(context, Message.Message): - # See if it's got an Approved: header, either in the headers, - # or in the first text/plain section of the response. For - # robustness, we'll accept Approve: as well. - approved = context.get('Approved', context.get('Approve')) - if not approved: - try: - subpart = list(email.Iterators.typed_subpart_iterator( - context, 'text', 'plain'))[0] - except IndexError: - subpart = None - if subpart: - s = StringIO(subpart.get_payload()) - while True: - line = s.readline() - if not line: - break - if not line.strip(): - continue - i = line.find(':') - if i > 0: - if (line[:i].lower() == 'approve' or - line[:i].lower() == 'approved'): - # then - approved = line[i+1:].strip() - break - # Is there an approved header? - if approved is not None: - # Does it match the list password? Note that we purposefully - # do not allow the site password here. - if self.Authenticate([config.AuthListAdmin, - config.AuthListModerator], - approved) <> config.UnAuthorized: - action = config.APPROVE - else: - # The password didn't match. Re-pend the message and - # inform the list moderators about the problem. - self.pend_repend(cookie, rec) - raise Errors.MMBadPasswordError - else: - action = config.DISCARD - try: - self.HandleRequest(id, action) - except KeyError: - # Most likely because the message has already been disposed of - # via the admindb page. - elog.error('Could not process HELD_MESSAGE: %s', id) - return (op,) - elif op == Pending.RE_ENABLE: - member = data[1] - self.setDeliveryStatus(member, MemberAdaptor.ENABLED) - return op, member - else: - assert 0, 'Bad op: %s' % op - - def ConfirmUnsubscription(self, addr, lang=None, remote=None): - if lang is None: - lang = self.getMemberLanguage(addr) - cookie = self.pend_new(Pending.UNSUBSCRIPTION, addr) - confirmurl = '%s/%s' % (self.GetScriptURL('confirm', absolute=1), - cookie) - realname = self.real_name - if remote is not None: - by = " " + remote - remote = _(" from %(remote)s") - else: - by = "" - remote = "" - text = Utils.maketext( - 'unsub.txt', - {'email' : addr, - 'listaddr' : self.GetListEmail(), - 'listname' : realname, - 'cookie' : cookie, - 'requestaddr' : self.getListAddress('request'), - 'remote' : remote, - 'listadmin' : self.GetOwnerEmail(), - 'confirmurl' : confirmurl, - }, lang=lang, mlist=self) - msg = Message.UserNotification( - addr, self.GetRequestEmail(cookie), - text=text, lang=lang) - # BAW: See ChangeMemberAddress() for why we do it this way... - del msg['subject'] - msg['Subject'] = self.GetConfirmLeaveSubject(realname, cookie) - msg['Reply-To'] = self.GetRequestEmail(cookie) - msg.send(self) - - - # - # Miscellaneous stuff - # - - def HasAutoApprovedSender(self, sender): - """Returns True and logs if sender matches address or pattern - in subscribe_auto_approval. Otherwise returns False. - """ - auto_approve = False - if Utils.get_pattern(sender, self.subscribe_auto_approval): - auto_approve = True - vlog.info('%s: auto approved subscribe from %s', - self.internal_name(), sender) - return auto_approve - - - # - # Multilingual (i18n) support - # - def set_languages(self, *language_codes): - # XXX FIXME not to use a database entity directly. - from Mailman.database.model import Language - # Don't use the language_codes property because that will add the - # default server language. The effect would be that the default - # server language would never get added to the list's list of - # languages. - requested_codes = set(language_codes) - enabled_codes = set(config.languages.enabled_codes) - self.available_languages = [ - Language(code) for code in requested_codes & enabled_codes] - - def add_language(self, language_code): - self.available_languages.append(Language(language_code)) - - @property - def language_codes(self): - # Callers of this method expect a list of language codes - available_codes = set(self.available_languages) - enabled_codes = set(config.languages.enabled_codes) - codes = available_codes & enabled_codes - # If we don't add this, and the site admin has never added any - # language support to the list, then the general admin page may have a - # blank field where the list owner is supposed to chose the list's - # preferred language. - if config.DEFAULT_SERVER_LANGUAGE not in codes: - codes.add(config.DEFAULT_SERVER_LANGUAGE) - return list(codes) diff --git a/mailman/attic/SecurityManager.py b/mailman/attic/SecurityManager.py deleted file mode 100644 index 8d4a30592..000000000 --- a/mailman/attic/SecurityManager.py +++ /dev/null @@ -1,306 +0,0 @@ -# Copyright (C) 1998-2009 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 . - -"""Handle passwords and sanitize approved messages.""" - -# There are current 5 roles defined in Mailman, as codified in Defaults.py: -# user, list-creator, list-moderator, list-admin, site-admin. -# -# Here's how we do cookie based authentication. -# -# Each role (see above) has an associated password, which is currently the -# only way to authenticate a role (in the future, we'll authenticate a -# user and assign users to roles). -# -# Each cookie has the following ingredients: the authorization context's -# secret (i.e. the password, and a timestamp. We generate an SHA1 hex -# digest of these ingredients, which we call the 'mac'. We then marshal -# up a tuple of the timestamp and the mac, hexlify that and return that as -# a cookie keyed off the authcontext. Note that authenticating the user -# also requires the user's email address to be included in the cookie. -# -# The verification process is done in CheckCookie() below. It extracts -# the cookie, unhexlifies and unmarshals the tuple, extracting the -# timestamp. Using this, and the shared secret, the mac is calculated, -# and it must match the mac passed in the cookie. If so, they're golden, -# otherwise, access is denied. -# -# It is still possible for an adversary to attempt to brute force crack -# the password if they obtain the cookie, since they can extract the -# timestamp and create macs based on password guesses. They never get a -# cleartext version of the password though, so security rests on the -# difficulty and expense of retrying the cgi dialog for each attempt. It -# also relies on the security of SHA1. - -import os -import re -import sha -import time -import urllib -import Cookie -import logging -import marshal -import binascii - -from urlparse import urlparse - -from Mailman import Defaults -from Mailman import Errors -from Mailman import Utils -from Mailman import passwords -from Mailman.configuration import config - -log = logging.getLogger('mailman.error') -dlog = logging.getLogger('mailman.debug') - -SLASH = '/' - - - -class SecurityManager: - def AuthContextInfo(self, authcontext, user=None): - # authcontext may be one of AuthUser, AuthListModerator, - # AuthListAdmin, AuthSiteAdmin. Not supported is the AuthCreator - # context. - # - # user is ignored unless authcontext is AuthUser - # - # Return the authcontext's secret and cookie key. If the authcontext - # doesn't exist, return the tuple (None, None). If authcontext is - # AuthUser, but the user isn't a member of this mailing list, a - # NotAMemberError will be raised. If the user's secret is None, raise - # a MMBadUserError. - key = urllib.quote(self.fqdn_listname) + '+' - if authcontext == Defaults.AuthUser: - if user is None: - # A bad system error - raise TypeError('No user supplied for AuthUser context') - secret = self.getMemberPassword(user) - userdata = urllib.quote(Utils.ObscureEmail(user), safe='') - key += 'user+%s' % userdata - elif authcontext == Defaults.AuthListModerator: - secret = self.mod_password - key += 'moderator' - elif authcontext == Defaults.AuthListAdmin: - secret = self.password - key += 'admin' - # BAW: AuthCreator - elif authcontext == Defaults.AuthSiteAdmin: - sitepass = Utils.get_global_password() - if config.ALLOW_SITE_ADMIN_COOKIES and sitepass: - secret = sitepass - key = 'site' - else: - # BAW: this should probably hand out a site password based - # cookie, but that makes me a bit nervous, so just treat site - # admin as a list admin since there is currently no site - # admin-only functionality. - secret = self.password - key += 'admin' - else: - return None, None - return key, secret - - def Authenticate(self, authcontexts, response, user=None): - # Given a list of authentication contexts, check to see if the - # response matches one of the passwords. authcontexts must be a - # sequence, and if it contains the context AuthUser, then the user - # argument must not be None. - # - # Return the authcontext from the argument sequence that matches the - # response, or UnAuthorized. - for ac in authcontexts: - if ac == Defaults.AuthCreator: - ok = Utils.check_global_password(response, siteadmin=False) - if ok: - return Defaults.AuthCreator - elif ac == Defaults.AuthSiteAdmin: - ok = Utils.check_global_password(response) - if ok: - return Defaults.AuthSiteAdmin - elif ac == Defaults.AuthListAdmin: - # The password for the list admin and list moderator are not - # kept as plain text, but instead as an sha hexdigest. The - # response being passed in is plain text, so we need to - # digestify it first. - key, secret = self.AuthContextInfo(ac) - if secret is None: - continue - if passwords.check_response(secret, response): - return ac - elif ac == Defaults.AuthListModerator: - # The list moderator password must be sha'd - key, secret = self.AuthContextInfo(ac) - if secret and passwords.check_response(secret, response): - return ac - elif ac == Defaults.AuthUser: - if user is not None: - try: - if self.authenticateMember(user, response): - return ac - except Errors.NotAMemberError: - pass - else: - # What is this context??? - log.error('Bad authcontext: %s', ac) - raise ValueError('Bad authcontext: %s' % ac) - return Defaults.UnAuthorized - - def WebAuthenticate(self, authcontexts, response, user=None): - # Given a list of authentication contexts, check to see if the cookie - # contains a matching authorization, falling back to checking whether - # the response matches one of the passwords. authcontexts must be a - # sequence, and if it contains the context AuthUser, then the user - # argument should not be None. - # - # Returns a flag indicating whether authentication succeeded or not. - for ac in authcontexts: - ok = self.CheckCookie(ac, user) - if ok: - return True - # Check passwords - ac = self.Authenticate(authcontexts, response, user) - if ac: - print self.MakeCookie(ac, user) - return True - return False - - def _cookie_path(self): - script_name = os.environ.get('SCRIPT_NAME', '') - return SLASH.join(script_name.split(SLASH)[:-1]) + SLASH - - def MakeCookie(self, authcontext, user=None): - key, secret = self.AuthContextInfo(authcontext, user) - if key is None or secret is None or not isinstance(secret, basestring): - raise ValueError - # Timestamp - issued = int(time.time()) - # Get a digest of the secret, plus other information. - mac = sha.new(secret + repr(issued)).hexdigest() - # Create the cookie object. - c = Cookie.SimpleCookie() - c[key] = binascii.hexlify(marshal.dumps((issued, mac))) - c[key]['path'] = self._cookie_path() - # We use session cookies, so don't set 'expires' or 'max-age' keys. - # Set the RFC 2109 required header. - c[key]['version'] = 1 - return c - - def ZapCookie(self, authcontext, user=None): - # We can throw away the secret. - key, secret = self.AuthContextInfo(authcontext, user) - # Logout of the session by zapping the cookie. For safety both set - # max-age=0 (as per RFC2109) and set the cookie data to the empty - # string. - c = Cookie.SimpleCookie() - c[key] = '' - c[key]['path'] = self._cookie_path() - c[key]['max-age'] = 0 - # Don't set expires=0 here otherwise it'll force a persistent cookie - c[key]['version'] = 1 - return c - - def CheckCookie(self, authcontext, user=None): - # Two results can occur: we return 1 meaning the cookie authentication - # succeeded for the authorization context, we return 0 meaning the - # authentication failed. - # - # Dig out the cookie data, which better be passed on this cgi - # environment variable. If there's no cookie data, we reject the - # authentication. - cookiedata = os.environ.get('HTTP_COOKIE') - if not cookiedata: - return False - # We can't use the Cookie module here because it isn't liberal in what - # it accepts. Feed it a MM2.0 cookie along with a MM2.1 cookie and - # you get a CookieError. :(. All we care about is accessing the - # cookie data via getitem, so we'll use our own parser, which returns - # a dictionary. - c = parsecookie(cookiedata) - # If the user was not supplied, but the authcontext is AuthUser, we - # can try to glean the user address from the cookie key. There may be - # more than one matching key (if the user has multiple accounts - # subscribed to this list), but any are okay. - if authcontext == Defaults.AuthUser: - if user: - usernames = [user] - else: - usernames = [] - prefix = urllib.quote(self.fqdn_listname) + '+user+' - for k in c.keys(): - if k.startswith(prefix): - usernames.append(k[len(prefix):]) - # If any check out, we're golden. Note: '@'s are no longer legal - # values in cookie keys. - for user in [Utils.UnobscureEmail(u) for u in usernames]: - ok = self.__checkone(c, authcontext, user) - if ok: - return True - return False - else: - return self.__checkone(c, authcontext, user) - - def __checkone(self, c, authcontext, user): - # Do the guts of the cookie check, for one authcontext/user - # combination. - try: - key, secret = self.AuthContextInfo(authcontext, user) - except Errors.NotAMemberError: - return False - if key not in c or not isinstance(secret, basestring): - return False - # Undo the encoding we performed in MakeCookie() above. BAW: I - # believe this is safe from exploit because marshal can't be forced to - # load recursive data structures, and it can't be forced to execute - # any unexpected code. The worst that can happen is that either the - # client will have provided us bogus data, in which case we'll get one - # of the caught exceptions, or marshal format will have changed, in - # which case, the cookie decoding will fail. In either case, we'll - # simply request reauthorization, resulting in a new cookie being - # returned to the client. - try: - data = marshal.loads(binascii.unhexlify(c[key])) - issued, received_mac = data - except (EOFError, ValueError, TypeError, KeyError): - return False - # Make sure the issued timestamp makes sense - now = time.time() - if now < issued: - return False - # Calculate what the mac ought to be based on the cookie's timestamp - # and the shared secret. - mac = sha.new(secret + repr(issued)).hexdigest() - if mac <> received_mac: - return False - # Authenticated! - return True - - - -splitter = re.compile(';\s*') - -def parsecookie(s): - c = {} - for line in s.splitlines(): - for p in splitter.split(line): - try: - k, v = p.split('=', 1) - except ValueError: - pass - else: - c[k] = v - return c diff --git a/mailman/attic/bin/clone_member b/mailman/attic/bin/clone_member deleted file mode 100755 index 1f2a03aca..000000000 --- a/mailman/attic/bin/clone_member +++ /dev/null @@ -1,219 +0,0 @@ -#! @PYTHON@ -# -# Copyright (C) 1998,1999,2000,2001,2002 by the Free Software Foundation, Inc. -# -# This program 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 2 -# of the License, or (at your option) any later version. -# -# This program 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 this program; if not, write to the Free Software -# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - -"""Clone a member address. - -Cloning a member address means that a new member will be added who has all the -same options and passwords as the original member address. Note that this -operation is fairly trusting of the user who runs it -- it does no -verification to the new address, it does not send out a welcome message, etc. - -The existing member's subscription is usually not modified in any way. If you -want to remove the old address, use the -r flag. If you also want to change -any list admin addresses, use the -a flag. - -Usage: - clone_member [options] fromoldaddr tonewaddr - -Where: - - --listname=listname - -l listname - Check and modify only the named mailing lists. If -l is not given, - then all mailing lists are scanned from the address. Multiple -l - options can be supplied. - - --remove - -r - Remove the old address from the mailing list after it's been cloned. - - --admin - -a - Scan the list admin addresses for the old address, and clone or change - them too. - - --quiet - -q - Do the modifications quietly. - - --nomodify - -n - Print what would be done, but don't actually do it. Inhibits the - --quiet flag. - - --help - -h - Print this help message and exit. - - fromoldaddr (`from old address') is the old address of the user. tonewaddr - (`to new address') is the new address of the user. - -""" - -import sys -import getopt - -import paths -from Mailman import MailList -from Mailman import Utils -from Mailman import Errors -from Mailman.i18n import _ - - - -def usage(code, msg=''): - if code: - fd = sys.stderr - else: - fd = sys.stdout - print >> fd, _(__doc__) - if msg: - print >> fd, msg - sys.exit(code) - - - -def dolist(mlist, options): - SPACE = ' ' - if not options.quiet: - print _('processing mailing list:'), mlist.internal_name() - - # scan the list owners. TBD: mlist.owner keys should be lowercase? - oldowners = mlist.owner[:] - oldowners.sort() - if options.admintoo: - if not options.quiet: - print _(' scanning list owners:'), SPACE.join(oldowners) - newowners = {} - foundp = 0 - for owner in mlist.owner: - if options.lfromaddr == owner.lower(): - foundp = 1 - if options.remove: - continue - newowners[owner] = 1 - if foundp: - newowners[options.toaddr] = 1 - newowners = newowners.keys() - newowners.sort() - if options.modify: - mlist.owner = newowners - if not options.quiet: - if newowners <> oldowners: - print - print _(' new list owners:'), SPACE.join(newowners) - else: - print _('(no change)') - - # see if the fromaddr is a digest member or regular member - if options.lfromaddr in mlist.getDigestMemberKeys(): - digest = 1 - elif options.lfromaddr in mlist.getRegularMemberKeys(): - digest = 0 - else: - if not options.quiet: - print _(' address not found:'), options.fromaddr - return - - # Now change the membership address - try: - if options.modify: - mlist.changeMemberAddress(options.fromaddr, options.toaddr, - not options.remove) - if not options.quiet: - print _(' clone address added:'), options.toaddr - except Errors.MMAlreadyAMember: - if not options.quiet: - print _(' clone address is already a member:'), options.toaddr - - if options.remove: - print _(' original address removed:'), options.fromaddr - - - -def main(): - # default options - class Options: - listnames = None - remove = 0 - admintoo = 0 - quiet = 0 - modify = 1 - - # scan sysargs - try: - opts, args = getopt.getopt( - sys.argv[1:], 'arl:qnh', - ['admin', 'remove', 'listname=', 'quiet', 'nomodify', 'help']) - except getopt.error, msg: - usage(1, msg) - - options = Options() - for opt, arg in opts: - if opt in ('-h', '--help'): - usage(0) - elif opt in ('-q', '--quiet'): - options.quiet = 1 - elif opt in ('-n', '--nomodify'): - options.modify = 0 - elif opt in ('-a', '--admin'): - options.admintoo = 1 - elif opt in ('-r', '--remove'): - options.remove = 1 - elif opt in ('-l', '--listname'): - if options.listnames is None: - options.listnames = [] - options.listnames.append(arg.lower()) - - # further options and argument processing - if not options.modify: - options.quiet = 0 - - if len(args) <> 2: - usage(1) - fromaddr = args[0] - toaddr = args[1] - - # validate and normalize the target address - try: - Utils.ValidateEmail(toaddr) - except Errors.EmailAddressError: - usage(1, _('Not a valid email address: %(toaddr)s')) - lfromaddr = fromaddr.lower() - options.toaddr = toaddr - options.fromaddr = fromaddr - options.lfromaddr = lfromaddr - - if options.listnames is None: - options.listnames = Utils.list_names() - - for listname in options.listnames: - try: - mlist = MailList.MailList(listname) - except Errors.MMListError, e: - print _('Error opening list "%(listname)s", skipping.\n%(e)s') - continue - try: - dolist(mlist, options) - finally: - mlist.Save() - mlist.Unlock() - - -if __name__ == '__main__': - main() diff --git a/mailman/attic/bin/discard b/mailman/attic/bin/discard deleted file mode 100644 index c30198441..000000000 --- a/mailman/attic/bin/discard +++ /dev/null @@ -1,120 +0,0 @@ -#! @PYTHON@ -# -# Copyright (C) 2003 by the Free Software Foundation, Inc. -# -# This program 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 2 -# of the License, or (at your option) any later version. -# -# This program 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 this program; if not, write to the Free Software -# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - -"""Discard held messages. - -Usage: - discard [options] file ... - -Options: - --help / -h - Print this help message and exit. - - --quiet / -q - Don't print status messages. -""" - -# TODO: add command line arguments for specifying other actions than DISCARD, -# and also for specifying other __handlepost() arguments, i.e. comment, -# preserve, forward, addr - -import os -import re -import sys -import getopt - -import paths -from Mailman import mm_cfg -from Mailman.MailList import MailList -from Mailman.i18n import _ - -try: - True, False -except NameError: - True = 1 - False = 0 - -cre = re.compile(r'heldmsg-(?P.*)-(?P[0-9]+)\.(pck|txt)$') - - - -def usage(code, msg=''): - if code: - fd = sys.stderr - else: - fd = sys.stdout - print >> fd, _(__doc__) - if msg: - print >> fd, msg - sys.exit(code) - - - -def main(): - try: - opts, args = getopt.getopt(sys.argv[1:], 'hq', ['help', 'quiet']) - except getopt.error, msg: - usage(1, msg) - - quiet = False - for opt, arg in opts: - if opt in ('-h', '--help'): - usage(0) - elif opt in ('-q', '--quiet'): - quiet = True - - files = args - if not files: - print _('Nothing to do.') - - # Mapping from listnames to sequence of request ids - discards = {} - - # Cruise through all the named files, collating by mailing list. We'll - # lock the list once, process all holds for that list and move on. - for f in files: - basename = os.path.basename(f) - mo = cre.match(basename) - if not mo: - print >> sys.stderr, _('Ignoring non-held message: %(f)s') - continue - listname, id = mo.group('listname', 'id') - try: - id = int(id) - except (ValueError, TypeError): - print >> sys.stderr, _('Ignoring held msg w/bad id: %(f)s') - continue - discards.setdefault(listname, []).append(id) - - # Now do the discards - for listname, ids in discards.items(): - mlist = MailList(listname) - try: - for id in ids: - # No comment, no preserve, no forward, no forwarding address - mlist.HandleRequest(id, mm_cfg.DISCARD, '', False, False, '') - if not quiet: - print _('Discarded held msg #%(id)s for list %(listname)s') - mlist.Save() - finally: - mlist.Unlock() - - - -if __name__ == '__main__': - main() diff --git a/mailman/attic/bin/fix_url.py b/mailman/attic/bin/fix_url.py deleted file mode 100644 index 30618a1a3..000000000 --- a/mailman/attic/bin/fix_url.py +++ /dev/null @@ -1,93 +0,0 @@ -#! @PYTHON@ -# -# Copyright (C) 2001-2009 by the Free Software Foundation, Inc. -# -# This program 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 2 -# of the License, or (at your option) any later version. -# -# This program 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 this program; if not, write to the Free Software -# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, -# USA. - -"""Reset a list's web_page_url attribute to the default setting. - -This script is intended to be run as a bin/withlist script, i.e. - -% bin/withlist -l -r fix_url listname [options] - -Options: - -u urlhost - --urlhost=urlhost - Look up urlhost in the virtual host table and set the web_page_url and - host_name attributes of the list to the values found. This - essentially moves the list from one virtual domain to another. - - Without this option, the default web_page_url and host_name values are - used. - - -v / --verbose - Print what the script is doing. - -If run standalone, it prints this help text and exits. -""" - -import sys -import getopt - -import paths -from Mailman.configuration import config -from Mailman.i18n import _ - - - -def usage(code, msg=''): - print _(__doc__.replace('%', '%%')) - if msg: - print msg - sys.exit(code) - - - -def fix_url(mlist, *args): - try: - opts, args = getopt.getopt(args, 'u:v', ['urlhost=', 'verbose']) - except getopt.error, msg: - usage(1, msg) - - verbose = 0 - urlhost = mailhost = None - for opt, arg in opts: - if opt in ('-u', '--urlhost'): - urlhost = arg - elif opt in ('-v', '--verbose'): - verbose = 1 - - if urlhost: - web_page_url = config.DEFAULT_URL_PATTERN % urlhost - mailhost = config.VIRTUAL_HOSTS.get(urlhost.lower(), urlhost) - else: - web_page_url = config.DEFAULT_URL_PATTERN % config.DEFAULT_URL_HOST - mailhost = config.DEFAULT_EMAIL_HOST - - if verbose: - print _('Setting web_page_url to: %(web_page_url)s') - mlist.web_page_url = web_page_url - if verbose: - print _('Setting host_name to: %(mailhost)s') - mlist.host_name = mailhost - print _('Saving list') - mlist.Save() - mlist.Unlock() - - - -if __name__ == '__main__': - usage(0) diff --git a/mailman/attic/bin/list_admins b/mailman/attic/bin/list_admins deleted file mode 100644 index c628a42dc..000000000 --- a/mailman/attic/bin/list_admins +++ /dev/null @@ -1,101 +0,0 @@ -#! @PYTHON@ -# -# Copyright (C) 2001,2002 by the Free Software Foundation, Inc. -# -# This program 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 2 -# of the License, or (at your option) any later version. -# -# This program 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 this program; if not, write to the Free Software -# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - -"""List all the owners of a mailing list. - -Usage: %(program)s [options] listname ... - -Where: - - --all-vhost=vhost - -v=vhost - List the owners of all the mailing lists for the given virtual host. - - --all - -a - List the owners of all the mailing lists on this system. - - --help - -h - Print this help message and exit. - -`listname' is the name of the mailing list to print the owners of. You can -have more than one named list on the command line. -""" - -import sys -import getopt - -import paths -from Mailman import MailList, Utils -from Mailman import Errors -from Mailman.i18n import _ - -COMMASPACE = ', ' - -program = sys.argv[0] - - - -def usage(code, msg=''): - if code: - fd = sys.stderr - else: - fd = sys.stdout - print >> fd, _(__doc__) - if msg: - print >> fd, msg - sys.exit(code) - - - -def main(): - try: - opts, args = getopt.getopt(sys.argv[1:], 'hv:a', - ['help', 'all-vhost=', 'all']) - except getopt.error, msg: - usage(1, msg) - - listnames = args - vhost = None - for opt, arg in opts: - if opt in ('-h', '--help'): - usage(0) - elif opt in ('-a', '--all'): - listnames = Utils.list_names() - elif opt in ('-v', '--all-vhost'): - listnames = Utils.list_names() - vhost = arg - - for listname in listnames: - try: - mlist = MailList.MailList(listname, lock=0) - except Errors.MMListError, e: - print _('No such list: %(listname)s') - continue - - if vhost and vhost <> mlist.host_name: - continue - - owners = COMMASPACE.join(mlist.owner) - print _('List: %(listname)s, \tOwners: %(owners)s') - - - -if __name__ == '__main__': - main() diff --git a/mailman/attic/bin/msgfmt.py b/mailman/attic/bin/msgfmt.py deleted file mode 100644 index 8a2d4e66e..000000000 --- a/mailman/attic/bin/msgfmt.py +++ /dev/null @@ -1,203 +0,0 @@ -#! /usr/bin/env python -# -*- coding: iso-8859-1 -*- -# Written by Martin v. Löwis - -"""Generate binary message catalog from textual translation description. - -This program converts a textual Uniforum-style message catalog (.po file) into -a binary GNU catalog (.mo file). This is essentially the same function as the -GNU msgfmt program, however, it is a simpler implementation. - -Usage: msgfmt.py [OPTIONS] filename.po - -Options: - -o file - --output-file=file - Specify the output file to write to. If omitted, output will go to a - file named filename.mo (based off the input file name). - - -h - --help - Print this message and exit. - - -V - --version - Display version information and exit. -""" - -import sys -import os -import getopt -import struct -import array - -__version__ = "1.1" - -MESSAGES = {} - - - -def usage(code, msg=''): - print >> sys.stderr, __doc__ - if msg: - print >> sys.stderr, msg - sys.exit(code) - - - -def add(id, str, fuzzy): - "Add a non-fuzzy translation to the dictionary." - global MESSAGES - if not fuzzy and str: - MESSAGES[id] = str - - - -def generate(): - "Return the generated output." - global MESSAGES - keys = MESSAGES.keys() - # the keys are sorted in the .mo file - keys.sort() - offsets = [] - ids = strs = '' - for id in keys: - # For each string, we need size and file offset. Each string is NUL - # terminated; the NUL does not count into the size. - offsets.append((len(ids), len(id), len(strs), len(MESSAGES[id]))) - ids += id + '\0' - strs += MESSAGES[id] + '\0' - output = '' - # The header is 7 32-bit unsigned integers. We don't use hash tables, so - # the keys start right after the index tables. - # translated string. - keystart = 7*4+16*len(keys) - # and the values start after the keys - valuestart = keystart + len(ids) - koffsets = [] - voffsets = [] - # The string table first has the list of keys, then the list of values. - # Each entry has first the size of the string, then the file offset. - for o1, l1, o2, l2 in offsets: - koffsets += [l1, o1+keystart] - voffsets += [l2, o2+valuestart] - offsets = koffsets + voffsets - output = struct.pack("Iiiiiii", - 0x950412deL, # Magic - 0, # Version - len(keys), # # of entries - 7*4, # start of key index - 7*4+len(keys)*8, # start of value index - 0, 0) # size and offset of hash table - output += array.array("i", offsets).tostring() - output += ids - output += strs - return output - - - -def make(filename, outfile): - ID = 1 - STR = 2 - - # Compute .mo name from .po name and arguments - if filename.endswith('.po'): - infile = filename - else: - infile = filename + '.po' - if outfile is None: - outfile = os.path.splitext(infile)[0] + '.mo' - - try: - lines = open(infile).readlines() - except IOError, msg: - print >> sys.stderr, msg - sys.exit(1) - - section = None - fuzzy = 0 - - # Parse the catalog - lno = 0 - for l in lines: - lno += 1 - # If we get a comment line after a msgstr, this is a new entry - if l[0] == '#' and section == STR: - add(msgid, msgstr, fuzzy) - section = None - fuzzy = 0 - # Record a fuzzy mark - if l[:2] == '#,' and l.find('fuzzy'): - fuzzy = 1 - # Skip comments - if l[0] == '#': - continue - # Now we are in a msgid section, output previous section - if l.startswith('msgid'): - if section == STR: - add(msgid, msgstr, fuzzy) - section = ID - l = l[5:] - msgid = msgstr = '' - # Now we are in a msgstr section - elif l.startswith('msgstr'): - section = STR - l = l[6:] - # Skip empty lines - l = l.strip() - if not l: - continue - # XXX: Does this always follow Python escape semantics? - l = eval(l) - if section == ID: - msgid += l - elif section == STR: - msgstr += l - else: - print >> sys.stderr, 'Syntax error on %s:%d' % (infile, lno), \ - 'before:' - print >> sys.stderr, l - sys.exit(1) - # Add last entry - if section == STR: - add(msgid, msgstr, fuzzy) - - # Compute output - output = generate() - - try: - open(outfile,"wb").write(output) - except IOError,msg: - print >> sys.stderr, msg - - - -def main(): - try: - opts, args = getopt.getopt(sys.argv[1:], 'hVo:', - ['help', 'version', 'output-file=']) - except getopt.error, msg: - usage(1, msg) - - outfile = None - # parse options - for opt, arg in opts: - if opt in ('-h', '--help'): - usage(0) - elif opt in ('-V', '--version'): - print >> sys.stderr, "msgfmt.py", __version__ - sys.exit(0) - elif opt in ('-o', '--output-file'): - outfile = arg - # do it - if not args: - print >> sys.stderr, 'No input file given' - print >> sys.stderr, "Try `msgfmt --help' for more information." - return - - for filename in args: - make(filename, outfile) - - -if __name__ == '__main__': - main() diff --git a/mailman/attic/bin/po2templ.py b/mailman/attic/bin/po2templ.py deleted file mode 100644 index 86eae96b9..000000000 --- a/mailman/attic/bin/po2templ.py +++ /dev/null @@ -1,90 +0,0 @@ -#! @PYTHON@ -# -# Copyright (C) 2005-2009 by the Free Software Foundation, Inc. -# -# This program 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 2 -# of the License, or (at your option) any later version. -# -# This program 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 this program; if not, write to the Free Software -# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, -# USA. - -# Author: Tokio Kikuchi - - -"""po2templ.py - -Extract templates from language po file. - -Usage: po2templ.py languages -""" - -import re -import sys - -cre = re.compile('^#:\s*templates/en/(?P.*?):1') - - - -def do_lang(lang): - in_template = False - in_msg = False - msgstr = '' - fp = file('messages/%s/LC_MESSAGES/mailman.po' % lang) - try: - for line in fp: - m = cre.search(line) - if m: - in_template = True - in_msg = False - filename = m.group('filename') - outfilename = 'templates/%s/%s' % (lang, filename) - continue - if in_template and line.startswith('#,'): - if line.strip() == '#, fuzzy': - in_template = False - continue - if in_template and line.startswith('msgstr'): - line = line[7:] - in_msg = True - if in_msg: - if not line.strip(): - in_template = False - in_msg = False - if len(msgstr) > 1 and outfilename: - # exclude no translation ... 1 is for LF only - outfile = file(outfilename, 'w') - try: - outfile.write(msgstr) - outfile.write('\n') - finally: - outfile.close() - outfilename = '' - msgstr = '' - continue - msgstr += eval(line) - finally: - fp.close() - if len(msgstr) > 1 and outfilename: - # flush remaining msgstr (last template file) - outfile = file(outfilename, 'w') - try: - outfile.write(msgstr) - outfile.write('\n') - finally: - outfile.close() - - - -if __name__ == '__main__': - langs = sys.argv[1:] - for lang in langs: - do_lang(lang) diff --git a/mailman/attic/bin/pygettext.py b/mailman/attic/bin/pygettext.py deleted file mode 100644 index 84421ee8c..000000000 --- a/mailman/attic/bin/pygettext.py +++ /dev/null @@ -1,545 +0,0 @@ -#! @PYTHON@ -# Originally written by Barry Warsaw -# -# Minimally patched to make it even more xgettext compatible -# by Peter Funk - -"""pygettext -- Python equivalent of xgettext(1) - -Many systems (Solaris, Linux, Gnu) provide extensive tools that ease the -internationalization of C programs. Most of these tools are independent of -the programming language and can be used from within Python programs. Martin -von Loewis' work[1] helps considerably in this regard. - -There's one problem though; xgettext is the program that scans source code -looking for message strings, but it groks only C (or C++). Python introduces -a few wrinkles, such as dual quoting characters, triple quoted strings, and -raw strings. xgettext understands none of this. - -Enter pygettext, which uses Python's standard tokenize module to scan Python -source code, generating .pot files identical to what GNU xgettext[2] generates -for C and C++ code. From there, the standard GNU tools can be used. - -A word about marking Python strings as candidates for translation. GNU -xgettext recognizes the following keywords: gettext, dgettext, dcgettext, and -gettext_noop. But those can be a lot of text to include all over your code. -C and C++ have a trick: they use the C preprocessor. Most internationalized C -source includes a #define for gettext() to _() so that what has to be written -in the source is much less. Thus these are both translatable strings: - - gettext("Translatable String") - _("Translatable String") - -Python of course has no preprocessor so this doesn't work so well. Thus, -pygettext searches only for _() by default, but see the -k/--keyword flag -below for how to augment this. - - [1] http://www.python.org/workshops/1997-10/proceedings/loewis.html - [2] http://www.gnu.org/software/gettext/gettext.html - -NOTE: pygettext attempts to be option and feature compatible with GNU xgettext -where ever possible. However some options are still missing or are not fully -implemented. Also, xgettext's use of command line switches with option -arguments is broken, and in these cases, pygettext just defines additional -switches. - -Usage: pygettext [options] inputfile ... - -Options: - - -a - --extract-all - Extract all strings. - - -d name - --default-domain=name - Rename the default output file from messages.pot to name.pot. - - -E - --escape - Replace non-ASCII characters with octal escape sequences. - - -D - --docstrings - Extract module, class, method, and function docstrings. These do not - need to be wrapped in _() markers, and in fact cannot be for Python to - consider them docstrings. (See also the -X option). - - -h - --help - Print this help message and exit. - - -k word - --keyword=word - Keywords to look for in addition to the default set, which are: - %(DEFAULTKEYWORDS)s - - You can have multiple -k flags on the command line. - - -K - --no-default-keywords - Disable the default set of keywords (see above). Any keywords - explicitly added with the -k/--keyword option are still recognized. - - --no-location - Do not write filename/lineno location comments. - - -n - --add-location - Write filename/lineno location comments indicating where each - extracted string is found in the source. These lines appear before - each msgid. The style of comments is controlled by the -S/--style - option. This is the default. - - -o filename - --output=filename - Rename the default output file from messages.pot to filename. If - filename is `-' then the output is sent to standard out. - - -p dir - --output-dir=dir - Output files will be placed in directory dir. - - -S stylename - --style stylename - Specify which style to use for location comments. Two styles are - supported: - - Solaris # File: filename, line: line-number - GNU #: filename:line - - The style name is case insensitive. GNU style is the default. - - -v - --verbose - Print the names of the files being processed. - - -V - --version - Print the version of pygettext and exit. - - -w columns - --width=columns - Set width of output to columns. - - -x filename - --exclude-file=filename - Specify a file that contains a list of strings that are not be - extracted from the input files. Each string to be excluded must - appear on a line by itself in the file. - - -X filename - --no-docstrings=filename - Specify a file that contains a list of files (one per line) that - should not have their docstrings extracted. This is only useful in - conjunction with the -D option above. - -If `inputfile' is -, standard input is read. -""" - -import os -import sys -import time -import getopt -import tokenize -import operator - -# for selftesting -try: - import fintl - _ = fintl.gettext -except ImportError: - def _(s): return s - -__version__ = '1.4' - -default_keywords = ['_'] -DEFAULTKEYWORDS = ', '.join(default_keywords) - -EMPTYSTRING = '' - - - -# The normal pot-file header. msgmerge and Emacs's po-mode work better if it's -# there. -pot_header = _('''\ -# SOME DESCRIPTIVE TITLE. -# Copyright (C) YEAR ORGANIZATION -# FIRST AUTHOR , YEAR. -# -msgid "" -msgstr "" -"Project-Id-Version: PACKAGE VERSION\\n" -"POT-Creation-Date: %(time)s\\n" -"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\\n" -"Last-Translator: FULL NAME \\n" -"Language-Team: LANGUAGE \\n" -"MIME-Version: 1.0\\n" -"Content-Type: text/plain; charset=CHARSET\\n" -"Content-Transfer-Encoding: ENCODING\\n" -"Generated-By: pygettext.py %(version)s\\n" - -''') - - -def usage(code, msg=''): - if code: - fd = sys.stderr - else: - fd = sys.stdout - print >> fd, _(__doc__) % globals() - if msg: - print >> fd, msg - sys.exit(code) - - - -escapes = [] - -def make_escapes(pass_iso8859): - global escapes - if pass_iso8859: - # Allow iso-8859 characters to pass through so that e.g. 'msgid - # "H[o-umlaut]he"' would result not result in 'msgid "H\366he"'. - # Otherwise we escape any character outside the 32..126 range. - mod = 128 - else: - mod = 256 - for i in range(256): - if 32 <= (i % mod) <= 126: - escapes.append(chr(i)) - else: - escapes.append("\\%03o" % i) - escapes[ord('\\')] = '\\\\' - escapes[ord('\t')] = '\\t' - escapes[ord('\r')] = '\\r' - escapes[ord('\n')] = '\\n' - escapes[ord('\"')] = '\\"' - - -def escape(s): - global escapes - s = list(s) - for i in range(len(s)): - s[i] = escapes[ord(s[i])] - return EMPTYSTRING.join(s) - - -def safe_eval(s): - # unwrap quotes, safely - return eval(s, {'__builtins__':{}}, {}) - - -def normalize(s): - # This converts the various Python string types into a format that is - # appropriate for .po files, namely much closer to C style. - lines = s.split('\n') - if len(lines) == 1: - s = '"' + escape(s) + '"' - else: - if not lines[-1]: - del lines[-1] - lines[-1] = lines[-1] + '\n' - for i in range(len(lines)): - lines[i] = escape(lines[i]) - lineterm = '\\n"\n"' - s = '""\n"' + lineterm.join(lines) + '"' - return s - - - -class TokenEater: - def __init__(self, options): - self.__options = options - self.__messages = {} - self.__state = self.__waiting - self.__data = [] - self.__lineno = -1 - self.__freshmodule = 1 - self.__curfile = None - - def __call__(self, ttype, tstring, stup, etup, line): - # dispatch -## import token -## print >> sys.stderr, 'ttype:', token.tok_name[ttype], \ -## 'tstring:', tstring - self.__state(ttype, tstring, stup[0]) - - def __waiting(self, ttype, tstring, lineno): - opts = self.__options - # Do docstring extractions, if enabled - if opts.docstrings and not opts.nodocstrings.get(self.__curfile): - # module docstring? - if self.__freshmodule: - if ttype == tokenize.STRING: - self.__addentry(safe_eval(tstring), lineno, isdocstring=1) - self.__freshmodule = 0 - elif ttype not in (tokenize.COMMENT, tokenize.NL): - self.__freshmodule = 0 - return - # class docstring? - if ttype == tokenize.NAME and tstring in ('class', 'def'): - self.__state = self.__suiteseen - return - if ttype == tokenize.NAME and tstring in opts.keywords: - self.__state = self.__keywordseen - - def __suiteseen(self, ttype, tstring, lineno): - # ignore anything until we see the colon - if ttype == tokenize.OP and tstring == ':': - self.__state = self.__suitedocstring - - def __suitedocstring(self, ttype, tstring, lineno): - # ignore any intervening noise - if ttype == tokenize.STRING: - self.__addentry(safe_eval(tstring), lineno, isdocstring=1) - self.__state = self.__waiting - elif ttype not in (tokenize.NEWLINE, tokenize.INDENT, - tokenize.COMMENT): - # there was no class docstring - self.__state = self.__waiting - - def __keywordseen(self, ttype, tstring, lineno): - if ttype == tokenize.OP and tstring == '(': - self.__data = [] - self.__lineno = lineno - self.__state = self.__openseen - else: - self.__state = self.__waiting - - def __openseen(self, ttype, tstring, lineno): - if ttype == tokenize.OP and tstring == ')': - # We've seen the last of the translatable strings. Record the - # line number of the first line of the strings and update the list - # of messages seen. Reset state for the next batch. If there - # were no strings inside _(), then just ignore this entry. - if self.__data: - self.__addentry(EMPTYSTRING.join(self.__data)) - self.__state = self.__waiting - elif ttype == tokenize.STRING: - self.__data.append(safe_eval(tstring)) - # TBD: should we warn if we seen anything else? - - def __addentry(self, msg, lineno=None, isdocstring=0): - if lineno is None: - lineno = self.__lineno - if not msg in self.__options.toexclude: - entry = (self.__curfile, lineno) - self.__messages.setdefault(msg, {})[entry] = isdocstring - - def set_filename(self, filename): - self.__curfile = filename - self.__freshmodule = 1 - - def write(self, fp): - options = self.__options - timestamp = time.ctime(time.time()) - # The time stamp in the header doesn't have the same format as that - # generated by xgettext... - print >> fp, pot_header % {'time': timestamp, 'version': __version__} - # Sort the entries. First sort each particular entry's keys, then - # sort all the entries by their first item. - reverse = {} - for k, v in self.__messages.items(): - keys = v.keys() - keys.sort() - reverse.setdefault(tuple(keys), []).append((k, v)) - rkeys = reverse.keys() - rkeys.sort() - for rkey in rkeys: - rentries = reverse[rkey] - rentries.sort() - for k, v in rentries: - isdocstring = 0 - # If the entry was gleaned out of a docstring, then add a - # comment stating so. This is to aid translators who may wish - # to skip translating some unimportant docstrings. - if reduce(operator.__add__, v.values()): - isdocstring = 1 - # k is the message string, v is a dictionary-set of (filename, - # lineno) tuples. We want to sort the entries in v first by - # file name and then by line number. - v = v.keys() - v.sort() - if not options.writelocations: - pass - # location comments are different b/w Solaris and GNU: - elif options.locationstyle == options.SOLARIS: - for filename, lineno in v: - d = {'filename': filename, 'lineno': lineno} - print >>fp, _( - '# File: %(filename)s, line: %(lineno)d') % d - elif options.locationstyle == options.GNU: - # fit as many locations on one line, as long as the - # resulting line length doesn't exceeds 'options.width' - locline = '#:' - for filename, lineno in v: - d = {'filename': filename, 'lineno': lineno} - s = _(' %(filename)s:%(lineno)d') % d - if len(locline) + len(s) <= options.width: - locline = locline + s - else: - print >> fp, locline - locline = "#:" + s - if len(locline) > 2: - print >> fp, locline - if isdocstring: - print >> fp, '#, docstring' - print >> fp, 'msgid', normalize(k) - print >> fp, 'msgstr ""\n' - - - -def main(): - global default_keywords - try: - opts, args = getopt.getopt( - sys.argv[1:], - 'ad:DEhk:Kno:p:S:Vvw:x:X:', - ['extract-all', 'default-domain=', 'escape', 'help', - 'keyword=', 'no-default-keywords', - 'add-location', 'no-location', 'output=', 'output-dir=', - 'style=', 'verbose', 'version', 'width=', 'exclude-file=', - 'docstrings', 'no-docstrings', - ]) - except getopt.error, msg: - usage(1, msg) - - # for holding option values - class Options: - # constants - GNU = 1 - SOLARIS = 2 - # defaults - extractall = 0 # FIXME: currently this option has no effect at all. - escape = 0 - keywords = [] - outpath = '' - outfile = 'messages.pot' - writelocations = 1 - locationstyle = GNU - verbose = 0 - width = 78 - excludefilename = '' - docstrings = 0 - nodocstrings = {} - - options = Options() - locations = {'gnu' : options.GNU, - 'solaris' : options.SOLARIS, - } - - # parse options - for opt, arg in opts: - if opt in ('-h', '--help'): - usage(0) - elif opt in ('-a', '--extract-all'): - options.extractall = 1 - elif opt in ('-d', '--default-domain'): - options.outfile = arg + '.pot' - elif opt in ('-E', '--escape'): - options.escape = 1 - elif opt in ('-D', '--docstrings'): - options.docstrings = 1 - elif opt in ('-k', '--keyword'): - options.keywords.append(arg) - elif opt in ('-K', '--no-default-keywords'): - default_keywords = [] - elif opt in ('-n', '--add-location'): - options.writelocations = 1 - elif opt in ('--no-location',): - options.writelocations = 0 - elif opt in ('-S', '--style'): - options.locationstyle = locations.get(arg.lower()) - if options.locationstyle is None: - usage(1, _('Invalid value for --style: %s') % arg) - elif opt in ('-o', '--output'): - options.outfile = arg - elif opt in ('-p', '--output-dir'): - options.outpath = arg - elif opt in ('-v', '--verbose'): - options.verbose = 1 - elif opt in ('-V', '--version'): - print _('pygettext.py (xgettext for Python) %s') % __version__ - sys.exit(0) - elif opt in ('-w', '--width'): - try: - options.width = int(arg) - except ValueError: - usage(1, _('--width argument must be an integer: %s') % arg) - elif opt in ('-x', '--exclude-file'): - options.excludefilename = arg - elif opt in ('-X', '--no-docstrings'): - fp = open(arg) - try: - while 1: - line = fp.readline() - if not line: - break - options.nodocstrings[line[:-1]] = 1 - finally: - fp.close() - - # calculate escapes - make_escapes(options.escape) - - # calculate all keywords - options.keywords.extend(default_keywords) - - # initialize list of strings to exclude - if options.excludefilename: - try: - fp = open(options.excludefilename) - options.toexclude = fp.readlines() - fp.close() - except IOError: - print >> sys.stderr, _( - "Can't read --exclude-file: %s") % options.excludefilename - sys.exit(1) - else: - options.toexclude = [] - - # slurp through all the files - eater = TokenEater(options) - for filename in args: - if filename == '-': - if options.verbose: - print _('Reading standard input') - fp = sys.stdin - closep = 0 - else: - if options.verbose: - print _('Working on %s') % filename - fp = open(filename) - closep = 1 - try: - eater.set_filename(filename) - try: - tokenize.tokenize(fp.readline, eater) - except tokenize.TokenError, e: - print >> sys.stderr, '%s: %s, line %d, column %d' % ( - e[0], filename, e[1][0], e[1][1]) - finally: - if closep: - fp.close() - - # write the output - if options.outfile == '-': - fp = sys.stdout - closep = 0 - else: - if options.outpath: - options.outfile = os.path.join(options.outpath, options.outfile) - fp = open(options.outfile, 'w') - closep = 1 - try: - eater.write(fp) - finally: - if closep: - fp.close() - - -if __name__ == '__main__': - main() - # some more test strings - _(u'a unicode string') diff --git a/mailman/attic/bin/remove_members b/mailman/attic/bin/remove_members deleted file mode 100755 index a7b4ebb47..000000000 --- a/mailman/attic/bin/remove_members +++ /dev/null @@ -1,186 +0,0 @@ -#! @PYTHON@ -# -# Copyright (C) 1998-2005 by the Free Software Foundation, Inc. -# -# This program 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 2 -# of the License, or (at your option) any later version. -# -# This program 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 this program; if not, write to the Free Software -# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, -# USA. - -"""Remove members from a list. - -Usage: - remove_members [options] [listname] [addr1 ...] - -Options: - - --file=file - -f file - Remove member addresses found in the given file. If file is - `-', read stdin. - - --all - -a - Remove all members of the mailing list. - (mutually exclusive with --fromall) - - --fromall - Removes the given addresses from all the lists on this system - regardless of virtual domains if you have any. This option cannot be - used -a/--all. Also, you should not specify a listname when using - this option. - - --nouserack - -n - Don't send the user acknowledgements. If not specified, the list - default value is used. - - --noadminack - -N - Don't send the admin acknowledgements. If not specified, the list - default value is used. - - --help - -h - Print this help message and exit. - - listname is the name of the mailing list to use. - - addr1 ... are additional addresses to remove. -""" - -import sys -import getopt - -import paths -from Mailman import MailList -from Mailman import Utils -from Mailman import Errors -from Mailman.i18n import _ - -try: - True, False -except NameError: - True = 1 - False = 0 - - - -def usage(code, msg=''): - if code: - fd = sys.stderr - else: - fd = sys.stdout - print >> fd, _(__doc__) - if msg: - print >> fd, msg - sys.exit(code) - - -def ReadFile(filename): - lines = [] - if filename == "-": - fp = sys.stdin - closep = False - else: - fp = open(filename) - closep = True - lines = filter(None, [line.strip() for line in fp.readlines()]) - if closep: - fp.close() - return lines - - - -def main(): - try: - opts, args = getopt.getopt( - sys.argv[1:], 'naf:hN', - ['all', 'fromall', 'file=', 'help', 'nouserack', 'noadminack']) - except getopt.error, msg: - usage(1, msg) - - filename = None - all = False - alllists = False - # None means use list default - userack = None - admin_notif = None - - for opt, arg in opts: - if opt in ('-h', '--help'): - usage(0) - elif opt in ('-f', '--file'): - filename = arg - elif opt in ('-a', '--all'): - all = True - elif opt == '--fromall': - alllists = True - elif opt in ('-n', '--nouserack'): - userack = False - elif opt in ('-N', '--noadminack'): - admin_notif = False - - if len(args) < 1 and not (filename and alllists): - usage(1) - - # You probably don't want to delete all the users of all the lists -- Marc - if all and alllists: - usage(1) - - if alllists: - addresses = args - else: - listname = args[0].lower().strip() - addresses = args[1:] - - if alllists: - listnames = Utils.list_names() - else: - listnames = [listname] - - if filename: - try: - addresses = addresses + ReadFile(filename) - except IOError: - print _('Could not open file for reading: %(filename)s.') - - for listname in listnames: - try: - # open locked - mlist = MailList.MailList(listname) - except Errors.MMListError: - print _('Error opening list %(listname)s... skipping.') - continue - - if all: - addresses = mlist.getMembers() - - try: - for addr in addresses: - if not mlist.isMember(addr): - if not alllists: - print _('No such member: %(addr)s') - continue - mlist.ApprovedDeleteMember(addr, 'bin/remove_members', - admin_notif, userack) - if alllists: - print _("User `%(addr)s' removed from list: %(listname)s.") - mlist.Save() - finally: - mlist.Unlock() - - - -if __name__ == '__main__': - main() diff --git a/mailman/attic/bin/reset_pw.py b/mailman/attic/bin/reset_pw.py deleted file mode 100644 index 453c8b849..000000000 --- a/mailman/attic/bin/reset_pw.py +++ /dev/null @@ -1,83 +0,0 @@ -#! @PYTHON@ -# -# Copyright (C) 2004-2009 by the Free Software Foundation, Inc. -# -# This program 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 2 -# of the License, or (at your option) any later version. -# -# This program 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 this program; if not, write to the Free Software -# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - -# Inspired by Florian Weimer. - -"""Reset the passwords for members of a mailing list. - -This script resets all the passwords of a mailing list's members. It can also -be used to reset the lists of all members of all mailing lists, but it is your -responsibility to let the users know that their passwords have been changed. - -This script is intended to be run as a bin/withlist script, i.e. - -% bin/withlist -l -r reset_pw listname [options] - -Options: - -v / --verbose - Print what the script is doing. -""" - -import sys -import getopt - -import paths -from Mailman import Utils -from Mailman.i18n import _ - - - -def usage(code, msg=''): - if code: - fd = sys.stderr - else: - fd = sys.stdout - print >> fd, _(__doc__.replace('%', '%%')) - if msg: - print >> fd, msg - sys.exit(code) - - - -def reset_pw(mlist, *args): - try: - opts, args = getopt.getopt(args, 'v', ['verbose']) - except getopt.error, msg: - usage(1, msg) - - verbose = False - for opt, args in opts: - if opt in ('-v', '--verbose'): - verbose = True - - listname = mlist.internal_name() - if verbose: - print _('Changing passwords for list: %(listname)s') - - for member in mlist.getMembers(): - randompw = Utils.MakeRandomPassword() - mlist.setMemberPassword(member, randompw) - if verbose: - print _('New password for member %(member)40s: %(randompw)s') - - mlist.Save() - - - -if __name__ == '__main__': - usage(0) diff --git a/mailman/attic/bin/sync_members b/mailman/attic/bin/sync_members deleted file mode 100755 index 4a21624c1..000000000 --- a/mailman/attic/bin/sync_members +++ /dev/null @@ -1,286 +0,0 @@ -#! @PYTHON@ -# -# Copyright (C) 1998-2003 by the Free Software Foundation, Inc. -# -# This program 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 2 -# of the License, or (at your option) any later version. -# -# This program 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 this program; if not, write to the Free Software -# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - -"""Synchronize a mailing list's membership with a flat file. - -This script is useful if you have a Mailman mailing list and a sendmail -:include: style list of addresses (also as is used in Majordomo). For every -address in the file that does not appear in the mailing list, the address is -added. For every address in the mailing list that does not appear in the -file, the address is removed. Other options control what happens when an -address is added or removed. - -Usage: %(PROGRAM)s [options] -f file listname - -Where `options' are: - - --no-change - -n - Don't actually make the changes. Instead, print out what would be - done to the list. - - --welcome-msg[=] - -w[=] - Sets whether or not to send the newly added members a welcome - message, overriding whatever the list's `send_welcome_msg' setting - is. With -w=yes or -w, the welcome message is sent. With -w=no, no - message is sent. - - --goodbye-msg[=] - -g[=] - Sets whether or not to send the goodbye message to removed members, - overriding whatever the list's `send_goodbye_msg' setting is. With - -g=yes or -g, the goodbye message is sent. With -g=no, no message is - sent. - - --digest[=] - -d[=] - Selects whether to make newly added members receive messages in - digests. With -d=yes or -d, they become digest members. With -d=no - (or if no -d option given) they are added as regular members. - - --notifyadmin[=] - -a[=] - Specifies whether the admin should be notified for each subscription - or unsubscription. If you're adding a lot of addresses, you - definitely want to turn this off! With -a=yes or -a, the admin is - notified. With -a=no, the admin is not notified. With no -a option, - the default for the list is used. - - --file - -f - This option is required. It specifies the flat file to synchronize - against. Email addresses must appear one per line. If filename is - `-' then stdin is used. - - --help - -h - Print this message. - - listname - Required. This specifies the list to synchronize. -""" - -import sys - -import paths -# Import this /after/ paths so that the sys.path is properly hacked -import email.Utils - -from Mailman import MailList -from Mailman import Errors -from Mailman import Utils -from Mailman.UserDesc import UserDesc -from Mailman.i18n import _ - - - -PROGRAM = sys.argv[0] - -def usage(code, msg=''): - if code: - fd = sys.stderr - else: - fd = sys.stdout - print >> fd, _(__doc__) - if msg: - print >> fd, msg - sys.exit(code) - - - -def yesno(opt): - i = opt.find('=') - yesno = opt[i+1:].lower() - if yesno in ('y', 'yes'): - return 1 - elif yesno in ('n', 'no'): - return 0 - else: - usage(1, _('Bad choice: %(yesno)s')) - # no return - - -def main(): - dryrun = 0 - digest = 0 - welcome = None - goodbye = None - filename = None - listname = None - notifyadmin = None - - # TBD: can't use getopt with this command line syntax, which is broken and - # should be changed to be getopt compatible. - i = 1 - while i < len(sys.argv): - opt = sys.argv[i] - if opt in ('-h', '--help'): - usage(0) - elif opt in ('-n', '--no-change'): - dryrun = 1 - i += 1 - print _('Dry run mode') - elif opt in ('-d', '--digest'): - digest = 1 - i += 1 - elif opt.startswith('-d=') or opt.startswith('--digest='): - digest = yesno(opt) - i += 1 - elif opt in ('-w', '--welcome-msg'): - welcome = 1 - i += 1 - elif opt.startswith('-w=') or opt.startswith('--welcome-msg='): - welcome = yesno(opt) - i += 1 - elif opt in ('-g', '--goodbye-msg'): - goodbye = 1 - i += 1 - elif opt.startswith('-g=') or opt.startswith('--goodbye-msg='): - goodbye = yesno(opt) - i += 1 - elif opt in ('-f', '--file'): - if filename is not None: - usage(1, _('Only one -f switch allowed')) - try: - filename = sys.argv[i+1] - except IndexError: - usage(1, _('No argument to -f given')) - i += 2 - elif opt in ('-a', '--notifyadmin'): - notifyadmin = 1 - i += 1 - elif opt.startswith('-a=') or opt.startswith('--notifyadmin='): - notifyadmin = yesno(opt) - i += 1 - elif opt[0] == '-': - usage(1, _('Illegal option: %(opt)s')) - else: - try: - listname = sys.argv[i].lower() - i += 1 - except IndexError: - usage(1, _('No listname given')) - break - - if listname is None or filename is None: - usage(1, _('Must have a listname and a filename')) - - # read the list of addresses to sync to from the file - if filename == '-': - filemembers = sys.stdin.readlines() - else: - try: - fp = open(filename) - except IOError, (code, msg): - usage(1, _('Cannot read address file: %(filename)s: %(msg)s')) - try: - filemembers = fp.readlines() - finally: - fp.close() - - # strip out lines we don't care about, they are comments (# in first - # non-whitespace) or are blank - for i in range(len(filemembers)-1, -1, -1): - addr = filemembers[i].strip() - if addr == '' or addr[:1] == '#': - del filemembers[i] - print _('Ignore : %(addr)30s') - - # first filter out any invalid addresses - filemembers = email.Utils.getaddresses(filemembers) - invalid = 0 - for name, addr in filemembers: - try: - Utils.ValidateEmail(addr) - except Errors.EmailAddressError: - print _('Invalid : %(addr)30s') - invalid = 1 - if invalid: - print _('You must fix the preceding invalid addresses first.') - sys.exit(1) - - # get the locked list object - try: - mlist = MailList.MailList(listname) - except Errors.MMListError, e: - print _('No such list: %(listname)s') - sys.exit(1) - - try: - # Get the list of addresses currently subscribed - addrs = {} - needsadding = {} - matches = {} - for addr in mlist.getMemberCPAddresses(mlist.getMembers()): - addrs[addr.lower()] = addr - - for name, addr in filemembers: - # Any address found in the file that is also in the list can be - # ignored. If not found in the list, it must be added later. - laddr = addr.lower() - if addrs.has_key(laddr): - del addrs[laddr] - matches[laddr] = 1 - elif not matches.has_key(laddr): - needsadding[laddr] = (name, addr) - - if not needsadding and not addrs: - print _('Nothing to do.') - sys.exit(0) - - enc = sys.getdefaultencoding() - # addrs contains now all the addresses that need removing - for laddr, (name, addr) in needsadding.items(): - pw = Utils.MakeRandomPassword() - # should not already be subscribed, otherwise our test above is - # broken. Bogosity is if the address is listed in the file more - # than once. Second and subsequent ones trigger an - # MMAlreadyAMember error. Just catch it and go on. - userdesc = UserDesc(addr, name, pw, digest) - try: - if not dryrun: - mlist.ApprovedAddMember(userdesc, welcome, notifyadmin) - s = email.Utils.formataddr((name, addr)).encode(enc, 'replace') - print _('Added : %(s)s') - except Errors.MMAlreadyAMember: - pass - - for laddr, addr in addrs.items(): - # Should be a member, otherwise our test above is broken - name = mlist.getMemberName(laddr) or '' - if not dryrun: - try: - mlist.ApprovedDeleteMember(addr, admin_notif=notifyadmin, - userack=goodbye) - except Errors.NotAMemberError: - # This can happen if the address is illegal (i.e. can't be - # parsed by email.Utils.parseaddr()) but for legacy - # reasons is in the database. Use a lower level remove to - # get rid of this member's entry - mlist.removeMember(addr) - s = email.Utils.formataddr((name, addr)).encode(enc, 'replace') - print _('Removed: %(s)s') - - mlist.Save() - finally: - mlist.Unlock() - - -if __name__ == '__main__': - main() diff --git a/mailman/attic/bin/templ2pot.py b/mailman/attic/bin/templ2pot.py deleted file mode 100644 index 0253cc2cd..000000000 --- a/mailman/attic/bin/templ2pot.py +++ /dev/null @@ -1,120 +0,0 @@ -#! @PYTHON@ -# Code stolen from pygettext.py -# by Tokio Kikuchi - -"""templ2pot.py -- convert mailman template (en) to pot format. - -Usage: templ2pot.py inputfile ... - -Options: - - -h, --help - -Inputfiles are english templates. Outputs are written to stdout. -""" - -import sys -import getopt - - - -try: - import paths - from Mailman.i18n import _ -except ImportError: - def _(s): return s - -EMPTYSTRING = '' - - - -def usage(code, msg=''): - if code: - fd = sys.stderr - else: - fd = sys.stdout - print >> fd, _(__doc__) % globals() - if msg: - print >> fd, msg - sys.exit(code) - - - -escapes = [] - -def make_escapes(pass_iso8859): - global escapes - if pass_iso8859: - # Allow iso-8859 characters to pass through so that e.g. 'msgid - # "H[o-umlaut]he"' would result not result in 'msgid "H\366he"'. - # Otherwise we escape any character outside the 32..126 range. - mod = 128 - else: - mod = 256 - for i in range(256): - if 32 <= (i % mod) <= 126: - escapes.append(chr(i)) - else: - escapes.append("\\%03o" % i) - escapes[ord('\\')] = '\\\\' - escapes[ord('\t')] = '\\t' - escapes[ord('\r')] = '\\r' - escapes[ord('\n')] = '\\n' - escapes[ord('\"')] = '\\"' - - -def escape(s): - global escapes - s = list(s) - for i in range(len(s)): - s[i] = escapes[ord(s[i])] - return EMPTYSTRING.join(s) - - -def normalize(s): - # This converts the various Python string types into a format that is - # appropriate for .po files, namely much closer to C style. - lines = s.splitlines() - if len(lines) == 1: - s = '"' + escape(s) + '"' - else: - if not lines[-1]: - del lines[-1] - lines[-1] = lines[-1] + '\n' - for i in range(len(lines)): - lines[i] = escape(lines[i]) - lineterm = '\\n"\n"' - s = '""\n"' + lineterm.join(lines) + '"' - return s - - - -def main(): - try: - opts, args = getopt.getopt( - sys.argv[1:], - 'h', - ['help',] - ) - except getopt.error, msg: - usage(1, msg) - - # parse options - for opt, arg in opts: - if opt in ('-h', '--help'): - usage(0) - - # calculate escapes - make_escapes(0) - - for filename in args: - print '#: %s:1' % filename - s = file(filename).read() - print '#, template' - print 'msgid', normalize(s) - print 'msgstr ""\n' - - - -if __name__ == '__main__': - main() diff --git a/mailman/attic/bin/transcheck b/mailman/attic/bin/transcheck deleted file mode 100755 index 73910e771..000000000 --- a/mailman/attic/bin/transcheck +++ /dev/null @@ -1,412 +0,0 @@ -#! @PYTHON@ -# -# transcheck - (c) 2002 by Simone Piunno -# -# This program is free software; you can redistribute it and/or modify it -# under the terms of the version 2.0 of the GNU General Public License as -# published by the Free Software Foundation. -# -# This program 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 this program; if not, write to the Free Software Foundation, Inc., -# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - -""" -Check a given Mailman translation, making sure that variables and -tags referenced in translation are the same variables and tags in -the original templates and catalog. - -Usage: - -cd $MAILMAN_DIR -%(program)s [-q] - -Where is your country code (e.g. 'it' for Italy) and -q is -to ask for a brief summary. -""" - -import sys -import re -import os -import getopt - -import paths -from Mailman.i18n import _ - -program = sys.argv[0] - - - -def usage(code, msg=''): - if code: - fd = sys.stderr - else: - fd = sys.stdout - print >> fd, _(__doc__) - if msg: - print >> fd, msg - sys.exit(code) - - - -class TransChecker: - "check a translation comparing with the original string" - def __init__(self, regexp, escaped=None): - self.dict = {} - self.errs = [] - self.regexp = re.compile(regexp) - self.escaped = None - if escaped: - self.escaped = re.compile(escaped) - - def checkin(self, string): - "scan a string from the original file" - for key in self.regexp.findall(string): - if self.escaped and self.escaped.match(key): - continue - if self.dict.has_key(key): - self.dict[key] += 1 - else: - self.dict[key] = 1 - - def checkout(self, string): - "scan a translated string" - for key in self.regexp.findall(string): - if self.escaped and self.escaped.match(key): - continue - if self.dict.has_key(key): - self.dict[key] -= 1 - else: - self.errs.append( - "%(key)s was not found" % - { 'key' : key } - ) - - def computeErrors(self): - "check for differences between checked in and checked out" - for key in self.dict.keys(): - if self.dict[key] < 0: - self.errs.append( - "Too much %(key)s" % - { 'key' : key } - ) - if self.dict[key] > 0: - self.errs.append( - "Too few %(key)s" % - { 'key' : key } - ) - return self.errs - - def status(self): - if self.errs: - return "FAILED" - else: - return "OK" - - def errorsAsString(self): - msg = "" - for err in self.errs: - msg += " - %(err)s" % { 'err': err } - return msg - - def reset(self): - self.dict = {} - self.errs = [] - - - -class POParser: - "parse a .po file extracting msgids and msgstrs" - def __init__(self, filename=""): - self.status = 0 - self.files = [] - self.msgid = "" - self.msgstr = "" - self.line = 1 - self.f = None - self.esc = { "n": "\n", "r": "\r", "t": "\t" } - if filename: - self.f = open(filename) - - def open(self, filename): - self.f = open(filename) - - def close(self): - self.f.close() - - def parse(self): - """States table for the finite-states-machine parser: - 0 idle - 1 filename-or-comment - 2 msgid - 3 msgstr - 4 end - """ - # each time we can safely re-initialize those vars - self.files = [] - self.msgid = "" - self.msgstr = "" - - - # can't continue if status == 4, this is a dead status - if self.status == 4: - return 0 - - while 1: - # continue scanning, char-by-char - c = self.f.read(1) - if not c: - # EOF -> maybe we have a msgstr to save? - self.status = 4 - if self.msgstr: - return 1 - else: - return 0 - - # keep the line count up-to-date - if c == "\n": - self.line += 1 - - # a pound was detected the previous char... - if self.status == 1: - if c == ":": - # was a line of filenames - row = self.f.readline() - self.files += row.split() - self.line += 1 - elif c == "\n": - # was a single pount on the line - pass - else: - # was a comment... discard - self.f.readline() - self.line += 1 - # in every case, we switch to idle status - self.status = 0; - continue - - # in idle status we search for a '#' or for a 'm' - if self.status == 0: - if c == "#": - # this could be a comment or a filename - self.status = 1; - continue - elif c == "m": - # this should be a msgid start... - s = self.f.read(4) - assert s == "sgid" - # so now we search for a '"' - self.status = 2 - continue - # in idle only those other chars are possibile - assert c in [ "\n", " ", "\t" ] - - # searching for the msgid string - if self.status == 2: - if c == "\n": - # a double LF is not possible here - c = self.f.read(1) - assert c != "\n" - if c == "\"": - # ok, this is the start of the string, - # now search for the end - while 1: - c = self.f.read(1) - if not c: - # EOF, bailout - self.status = 4 - return 0 - if c == "\\": - # a quoted char... - c = self.f.read(1) - if self.esc.has_key(c): - self.msgid += self.esc[c] - else: - self.msgid += c - continue - if c == "\"": - # end of string found - break - # a normal char, add it - self.msgid += c - if c == "m": - # this should be a msgstr identifier - s = self.f.read(5) - assert s == "sgstr" - # ok, now search for the msgstr string - self.status = 3 - - # searching for the msgstr string - if self.status == 3: - if c == "\n": - # a double LF is the end of the msgstr! - c = self.f.read(1) - if c == "\n": - # ok, time to go idle and return - self.status = 0 - self.line += 1 - return 1 - if c == "\"": - # start of string found - while 1: - c = self.f.read(1) - if not c: - # EOF, bail out - self.status = 4 - return 1 - if c == "\\": - # a quoted char... - c = self.f.read(1) - if self.esc.has_key(c): - self.msgid += self.esc[c] - else: - self.msgid += c - continue - if c == "\"": - # end of string - break - # a normal char, add it - self.msgstr += c - - - - -def check_file(translatedFile, originalFile, html=0, quiet=0): - """check a translated template against the original one - search also tags if html is not zero""" - - if html: - c = TransChecker("(%%|%\([^)]+\)[0-9]*[sd]|]+>)", "^%%$") - else: - c = TransChecker("(%%|%\([^)]+\)[0-9]*[sd])", "^%%$") - - try: - f = open(originalFile) - except IOError: - if not quiet: - print " - Can'open original file " + originalFile - return 1 - - while 1: - line = f.readline() - if not line: break - c.checkin(line) - - f.close() - - try: - f = open(translatedFile) - except IOError: - if not quiet: - print " - Can'open translated file " + translatedFile - return 1 - - while 1: - line = f.readline() - if not line: break - c.checkout(line) - - f.close() - - n = 0 - msg = "" - for desc in c.computeErrors(): - n +=1 - if not quiet: - print " - %(desc)s" % { 'desc': desc } - return n - - - -def check_po(file, quiet=0): - "scan the po file comparing msgids with msgstrs" - n = 0 - p = POParser(file) - c = TransChecker("(%%|%\([^)]+\)[0-9]*[sdu]|%[0-9]*[sdu])", "^%%$") - while p.parse(): - if p.msgstr: - c.reset() - c.checkin(p.msgid) - c.checkout(p.msgstr) - for desc in c.computeErrors(): - n += 1 - if not quiet: - print " - near line %(line)d %(file)s: %(desc)s" % { - 'line': p.line, - 'file': p.files, - 'desc': desc - } - p.close() - return n - - -def main(): - try: - opts, args = getopt.getopt(sys.argv[1:], 'qh', ['quiet', 'help']) - except getopt.error, msg: - usage(1, msg) - - quiet = 0 - for opt, arg in opts: - if opt in ('-h', '--help'): - usage(0) - elif opt in ('-q', '--quiet'): - quiet = 1 - - if len(args) <> 1: - usage(1) - - lang = args[0] - - isHtml = re.compile("\.html$"); - isTxt = re.compile("\.txt$"); - - numerrors = 0 - numfiles = 0 - try: - files = os.listdir("templates/" + lang + "/") - except: - print "can't open templates/%s/" % lang - for file in files: - fileEN = "templates/en/" + file - fileIT = "templates/" + lang + "/" + file - errlist = [] - if isHtml.search(file): - if not quiet: - print "HTML checking " + fileIT + "... " - n = check_file(fileIT, fileEN, html=1, quiet=quiet) - if n: - numerrors += n - numfiles += 1 - elif isTxt.search(file): - if not quiet: - print "TXT checking " + fileIT + "... " - n = check_file(fileIT, fileEN, html=0, quiet=quiet) - if n: - numerrors += n - numfiles += 1 - - else: - continue - - file = "messages/" + lang + "/LC_MESSAGES/mailman.po" - if not quiet: - print "PO checking " + file + "... " - n = check_po(file, quiet=quiet) - if n: - numerrors += n - numfiles += 1 - - if quiet: - print "%(errs)u warnings in %(files)u files" % { - 'errs': numerrors, - 'files': numfiles - } - - -if __name__ == '__main__': - main() diff --git a/mailman/bin/__init__.py b/mailman/bin/__init__.py deleted file mode 100644 index d61693c5e..000000000 --- a/mailman/bin/__init__.py +++ /dev/null @@ -1,61 +0,0 @@ -# Copyright (C) 2007-2009 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 . - -__all__ = [ - 'add_members', - 'arch', - 'bounces', - 'bumpdigests', - 'check_perms', - 'checkdbs', - 'cleanarch', - 'config_list', - 'confirm', - 'create_list', - 'disabled', - 'dumpdb', - 'export', - 'find_member', - 'gate_news', - 'genaliases', - 'import', - 'inject', - 'join', - 'leave', - 'list_lists', - 'list_members', - 'list_owners', - 'mailmanctl', - 'make_instance', - 'master', - 'mmsitepass', - 'nightly_gzip', - 'owner', - 'post', - 'qrunner', - 'remove_list', - 'request', - 'senddigests', - 'set_members', - 'show_config', - 'show_qfiles', - 'testall', - 'unshunt', - 'update', - 'version', - 'withlist', - ] diff --git a/mailman/bin/add_members.py b/mailman/bin/add_members.py deleted file mode 100644 index 9c87f4af9..000000000 --- a/mailman/bin/add_members.py +++ /dev/null @@ -1,186 +0,0 @@ -# Copyright (C) 1998-2009 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 . - -import os -import sys -import codecs - -from cStringIO import StringIO -from email.utils import parseaddr - -from mailman import Message -from mailman import Utils -from mailman import i18n -from mailman.app.membership import add_member -from mailman.config import config -from mailman.core import errors -from mailman.interfaces.member import AlreadySubscribedError, DeliveryMode -from mailman.options import SingleMailingListOptions - -_ = i18n._ - - - -class ScriptOptions(SingleMailingListOptions): - usage=_("""\ -%prog [options] - -Add members to a list. 'listname' is the name of the Mailman list you are -adding members to; the list must already exist. - -You must supply at least one of -r and -d options. At most one of the -files can be '-'. -""") - - def add_options(self): - super(ScriptOptions, self).add_options() - self.parser.add_option( - '-r', '--regular-members-file', - type='string', dest='regular', help=_("""\ -A file containing addresses of the members to be added, one address per line. -This list of people become non-digest members. If file is '-', read addresses -from stdin.""")) - self.parser.add_option( - '-d', '--digest-members-file', - type='string', dest='digest', help=_("""\ -Similar to -r, but these people become digest members.""")) - self.parser.add_option( - '-w', '--welcome-msg', - type='yesno', metavar='', help=_("""\ -Set whether or not to send the list members a welcome message, overriding -whatever the list's 'send_welcome_msg' setting is.""")) - self.parser.add_option( - '-a', '--admin-notify', - type='yesno', metavar='', help=_("""\ -Set whether or not to send the list administrators a notification on the -success/failure of these subscriptions, overriding whatever the list's -'admin_notify_mchanges' setting is.""")) - - def sanity_check(self): - if not self.options.listname: - self.parser.error(_('Missing listname')) - if len(self.arguments) > 0: - self.parser.print_error(_('Unexpected arguments')) - if self.options.regular is None and self.options.digest is None: - parser.error(_('At least one of -r or -d is required')) - if self.options.regular == '-' and self.options.digest == '-': - parser.error(_("-r and -d cannot both be '-'")) - - - -def readfile(filename): - if filename == '-': - fp = sys.stdin - else: - # XXX Need to specify other encodings. - fp = codecs.open(filename, encoding='utf-8') - # Strip all the lines of whitespace and discard blank lines - try: - return set(line.strip() for line in fp if line) - finally: - if fp is not sys.stdin: - fp.close() - - - -class Tee: - def __init__(self, outfp): - self._outfp = outfp - - def write(self, msg): - sys.stdout.write(msg) - self._outfp.write(msg) - - - -def addall(mlist, subscribers, delivery_mode, ack, admin_notify, outfp): - tee = Tee(outfp) - for subscriber in subscribers: - try: - fullname, address = parseaddr(subscriber) - # Watch out for the empty 8-bit string. - if not fullname: - fullname = u'' - password = Utils.MakeRandomPassword() - add_member(mlist, address, fullname, password, delivery_mode, - unicode(config.mailman.default_language)) - # XXX Support ack and admin_notify - except AlreadySubscribedError: - print >> tee, _('Already a member: $subscriber') - except errors.InvalidEmailAddress: - if not address: - print >> tee, _('Bad/Invalid email address: blank line') - else: - print >> tee, _('Bad/Invalid email address: $subscriber') - else: - print >> tee, _('Subscribing: $subscriber') - - - -def main(): - options = ScriptOptions() - options.initialize() - - fqdn_listname = options.options.listname - mlist = config.db.list_manager.get(fqdn_listname) - if mlist is None: - parser.error(_('No such list: $fqdn_listname')) - - # Set up defaults. - send_welcome_msg = (options.options.welcome_msg - if options.options.welcome_msg is not None - else mlist.send_welcome_msg) - admin_notify = (options.options.admin_notify - if options.options.admin_notify is not None - else mlist.admin_notify) - - with i18n.using_language(mlist.preferred_language): - if options.options.digest: - dmembers = readfile(options.options.digest) - else: - dmembers = set() - if options.options.regular: - nmembers = readfile(options.options.regular) - else: - nmembers = set() - - if not dmembers and not nmembers: - print _('Nothing to do.') - sys.exit(0) - - outfp = StringIO() - if nmembers: - addall(mlist, nmembers, DeliveryMode.regular, - send_welcome_msg, admin_notify, outfp) - - if dmembers: - addall(mlist, dmembers, DeliveryMode.mime_digests, - send_welcome_msg, admin_notify, outfp) - - config.db.commit() - - if admin_notify: - subject = _('$mlist.real_name subscription notification') - msg = Message.UserNotification( - mlist.owner, mlist.no_reply_address, subject, - outfp.getvalue(), mlist.preferred_language) - msg.send(mlist) - - - -if __name__ == '__main__': - main() diff --git a/mailman/bin/arch.py b/mailman/bin/arch.py deleted file mode 100644 index a27fa8d7f..000000000 --- a/mailman/bin/arch.py +++ /dev/null @@ -1,151 +0,0 @@ -# Copyright (C) 1998-2009 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 . - -import os -import sys -import errno -import shutil -import optparse - -from locknix.lockfile import Lock - -from mailman import i18n -from mailman.Archiver.HyperArch import HyperArchive -from mailman.Defaults import hours -from mailman.configuration import config -from mailman.initialize import initialize -from mailman.version import MAILMAN_VERSION - -_ = i18n._ - - - -def parseargs(): - parser = optparse.OptionParser(version=MAILMAN_VERSION, - usage=_("""\ -%%prog [options] listname [mbox] - -Rebuild a list's archive. - -Use this command to rebuild the archives for a mailing list. You may want to -do this if you edit some messages in an archive, or remove some messages from -an archive. - -Where 'mbox' is the path to a list's complete mbox archive. Usually this will -be some path in the archives/private directory. For example: - -% bin/arch mylist archives/private/mylist.mbox/mylist.mbox - -'mbox' is optional. If it is missing, it is calculated from the listname. -""")) - parser.add_option('-q', '--quiet', - dest='verbose', default=True, action='store_false', - help=_('Make the archiver output less verbose')) - parser.add_option('--wipe', - default=False, action='store_true', - help=_("""\ -First wipe out the original archive before regenerating. You usually want to -specify this argument unless you're generating the archive in chunks.""")) - parser.add_option('-s', '--start', - default=None, type='int', metavar='N', - help=_("""\ -Start indexing at article N, where article 0 is the first in the mbox. -Defaults to 0.""")) - parser.add_option('-e', '--end', - default=None, type='int', metavar='M', - help=_("""\ -End indexing at article M. This script is not very efficient with respect to -memory management, and for large archives, it may not be possible to index the -mbox entirely. For that reason, you can specify the start and end article -numbers.""")) - parser.add_option('-C', '--config', - help=_('Alternative configuration file to use')) - opts, args = parser.parse_args() - if len(args) < 1: - parser.print_help() - print >> sys.stderr, _('listname is required') - sys.exit(1) - if len(args) > 2: - parser.print_help() - print >> sys.stderr, _('Unexpected arguments') - sys.exit(1) - return parser, opts, args - - - -def main(): - parser, opts, args = parseargs() - initialize(opts.config) - - i18n.set_language(config.DEFAULT_SERVER_LANGUAGE) - - listname = args[0].lower().strip() - if len(args) < 2: - mbox = None - else: - mbox = args[1] - - # Open the mailing list object - mlist = config.list_manager.get(listname) - if mlist is None: - parser.error(_('No such list: $listname')) - if mbox is None: - mbox = mlist.ArchiveFileName() - - i18n.set_language(mlist.preferred_language) - # Lay claim to the archive's lock file. This is so no other post can - # mess up the archive while we're processing it. Try to pick a - # suitably long period of time for the lock lifetime even though we - # really don't know how long it will take. - # - # XXX processUnixMailbox() should refresh the lock. - lock_path = os.path.join(mlist.data_path, '.archiver.lck') - with Lock(lock_path, lifetime=int(hours(3))): - # Try to open mbox before wiping old archive. - try: - fp = open(mbox) - except IOError, e: - if e.errno == errno.ENOENT: - print >> sys.stderr, _('Cannot open mbox file: $mbox') - else: - print >> sys.stderr, e - sys.exit(1) - # Maybe wipe the old archives - if opts.wipe: - if mlist.scrub_nondigest: - # TK: save the attachments dir because they are not in mbox - saved = False - atchdir = os.path.join(mlist.archive_dir(), 'attachments') - savedir = os.path.join(mlist.archive_dir() + '.mbox', - 'attachments') - try: - os.rename(atchdir, savedir) - saved = True - except OSError, e: - if e.errno <> errno.ENOENT: - raise - shutil.rmtree(mlist.archive_dir()) - if mlist.scrub_nondigest and saved: - os.renames(savedir, atchdir) - - archiver = HyperArchive(mlist) - archiver.VERBOSE = opts.verbose - try: - archiver.processUnixMailbox(fp, opts.start, opts.end) - finally: - archiver.close() - fp.close() diff --git a/mailman/bin/bumpdigests.py b/mailman/bin/bumpdigests.py deleted file mode 100644 index b1ed37a21..000000000 --- a/mailman/bin/bumpdigests.py +++ /dev/null @@ -1,74 +0,0 @@ -# Copyright (C) 1998-2009 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 . - -import sys -import optparse - -from mailman import errors -from mailman import MailList -from mailman.configuration import config -from mailman.i18n import _ -from mailman.version import MAILMAN_VERSION - -# Work around known problems with some RedHat cron daemons -import signal -signal.signal(signal.SIGCHLD, signal.SIG_DFL) - - - -def parseargs(): - parser = optparse.OptionParser(version=MAILMAN_VERSION, - usage=_("""\ -%prog [options] [listname ...] - -Increment the digest volume number and reset the digest number to one. All -the lists named on the command line are bumped. If no list names are given, -all lists are bumped.""")) - parser.add_option('-C', '--config', - help=_('Alternative configuration file to use')) - opts, args = parser.parse_args() - return opts, args, parser - - - -def main(): - opts, args, parser = parseargs() - config.load(opts.config) - - listnames = set(args or config.list_manager.names) - if not listnames: - print _('Nothing to do.') - sys.exit(0) - - for listname in listnames: - try: - # Be sure the list is locked - mlist = MailList.MailList(listname) - except errors.MMListError, e: - parser.print_help() - print >> sys.stderr, _('No such list: $listname') - sys.exit(1) - try: - mlist.bump_digest_volume() - finally: - mlist.Save() - mlist.Unlock() - - - -if __name__ == '__main__': - main() diff --git a/mailman/bin/check_perms.py b/mailman/bin/check_perms.py deleted file mode 100644 index 4b75aa9f6..000000000 --- a/mailman/bin/check_perms.py +++ /dev/null @@ -1,408 +0,0 @@ -# Copyright (C) 1998-2009 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 . - -import os -import sys -import pwd -import grp -import errno -import optparse - -from stat import * - -from mailman.configuration import config -from mailman.i18n import _ -from mailman.version import MAILMAN_VERSION - - -# XXX Need to check the archives/private/*/database/* files - - - -class State: - FIX = False - VERBOSE = False - ERRORS = 0 - -STATE = State() - -DIRPERMS = S_ISGID | S_IRWXU | S_IRWXG | S_IROTH | S_IXOTH -QFILEPERMS = S_ISGID | S_IRWXU | S_IRWXG -PYFILEPERMS = S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH -ARTICLEFILEPERMS = S_IRUSR | S_IWUSR | S_IRGRP | S_IWGRP -MBOXPERMS = S_IRGRP | S_IWGRP | S_IRUSR | S_IWUSR -PRIVATEPERMS = QFILEPERMS - - - -def statmode(path): - return os.stat(path).st_mode - - -def statgidmode(path): - stat = os.stat(path) - return stat.st_mode, stat.st_gid - - -seen = {} - -# libc's getgrgid re-opens /etc/group each time :( -_gidcache = {} - -def getgrgid(gid): - data = _gidcache.get(gid) - if data is None: - data = grp.getgrgid(gid) - _gidcache[gid] = data - return data - - - -def checkwalk(arg, dirname, names): - # Short-circuit duplicates - if seen.has_key(dirname): - return - seen[dirname] = True - for name in names: - path = os.path.join(dirname, name) - if arg.VERBOSE: - print _(' checking gid and mode for $path') - try: - mode, gid = statgidmode(path) - except OSError, e: - if e.errno <> errno.ENOENT: raise - continue - if gid <> MAILMAN_GID: - try: - groupname = getgrgid(gid)[0] - except KeyError: - groupname = '' % gid - arg.ERRORS += 1 - print _( - '$path bad group (has: $groupname, expected $MAILMAN_GROUP)'), - if STATE.FIX: - print _('(fixing)') - os.chown(path, -1, MAILMAN_GID) - else: - print - # Most directories must be at least rwxrwsr-x. - # The private archive directory and database directory must be at - # least rwxrws---. Their 'other' permissions are checked in - # checkarchives() and checkarchivedbs() below. Their 'user' and - # 'group' permissions are checked here. - # The directories under qfiles should be rwxrws---. Their 'user' and - # 'group' permissions are checked here. Their 'other' permissions - # aren't checked. - private = config.PRIVATE_ARCHIVE_FILE_DIR - if path == private or ( - os.path.commonprefix((path, private)) == private - and os.path.split(path)[1] == 'database'): - # then... - targetperms = PRIVATEPERMS - elif (os.path.commonprefix((path, config.QUEUE_DIR)) - == config.QUEUE_DIR): - targetperms = QFILEPERMS - else: - targetperms = DIRPERMS - octperms = oct(targetperms) - if S_ISDIR(mode) and (mode & targetperms) <> targetperms: - arg.ERRORS += 1 - print _('directory permissions must be $octperms: $path'), - if STATE.FIX: - print _('(fixing)') - os.chmod(path, mode | targetperms) - else: - print - elif os.path.splitext(path)[1] in ('.py', '.pyc', '.pyo'): - octperms = oct(PYFILEPERMS) - if mode & PYFILEPERMS <> PYFILEPERMS: - print _('source perms must be $octperms: $path'), - arg.ERRORS += 1 - if STATE.FIX: - print _('(fixing)') - os.chmod(path, mode | PYFILEPERMS) - else: - print - elif path.endswith('-article'): - # Article files must be group writeable - octperms = oct(ARTICLEFILEPERMS) - if mode & ARTICLEFILEPERMS <> ARTICLEFILEPERMS: - print _('article db files must be $octperms: $path'), - arg.ERRORS += 1 - if STATE.FIX: - print _('(fixing)') - os.chmod(path, mode | ARTICLEFILEPERMS) - else: - print - - - -def checkall(): - # first check PREFIX - if STATE.VERBOSE: - prefix = config.PREFIX - print _('checking mode for $prefix') - dirs = {} - for d in (config.PREFIX, config.EXEC_PREFIX, config.VAR_PREFIX, - config.LOG_DIR): - dirs[d] = True - for d in dirs.keys(): - try: - mode = statmode(d) - except OSError, e: - if e.errno <> errno.ENOENT: raise - print _('WARNING: directory does not exist: $d') - continue - if (mode & DIRPERMS) <> DIRPERMS: - STATE.ERRORS += 1 - print _('directory must be at least 02775: $d'), - if STATE.FIX: - print _('(fixing)') - os.chmod(d, mode | DIRPERMS) - else: - print - # check all subdirs - os.path.walk(d, checkwalk, STATE) - - - -def checkarchives(): - private = config.PRIVATE_ARCHIVE_FILE_DIR - if STATE.VERBOSE: - print _('checking perms on $private') - # private archives must not be other readable - mode = statmode(private) - if mode & S_IROTH: - STATE.ERRORS += 1 - print _('$private must not be other-readable'), - if STATE.FIX: - print _('(fixing)') - os.chmod(private, mode & ~S_IROTH) - else: - print - # In addition, on a multiuser system you may want to hide the private - # archives so other users can't read them. - if mode & S_IXOTH: - print _("""\ -Warning: Private archive directory is other-executable (o+x). - This could allow other users on your system to read private archives. - If you're on a shared multiuser system, you should consult the - installation manual on how to fix this.""") - - - -def checkmboxfile(mboxdir): - absdir = os.path.join(config.PRIVATE_ARCHIVE_FILE_DIR, mboxdir) - for f in os.listdir(absdir): - if not f.endswith('.mbox'): - continue - mboxfile = os.path.join(absdir, f) - mode = statmode(mboxfile) - if (mode & MBOXPERMS) <> MBOXPERMS: - STATE.ERRORS = STATE.ERRORS + 1 - print _('mbox file must be at least 0660:'), mboxfile - if STATE.FIX: - print _('(fixing)') - os.chmod(mboxfile, mode | MBOXPERMS) - else: - print - - - -def checkarchivedbs(): - # The archives/private/listname/database file must not be other readable - # or executable otherwise those files will be accessible when the archives - # are public. That may not be a horrible breach, but let's close this off - # anyway. - for dir in os.listdir(config.PRIVATE_ARCHIVE_FILE_DIR): - if dir.endswith('.mbox'): - checkmboxfile(dir) - dbdir = os.path.join(config.PRIVATE_ARCHIVE_FILE_DIR, dir, 'database') - try: - mode = statmode(dbdir) - except OSError, e: - if e.errno not in (errno.ENOENT, errno.ENOTDIR): raise - continue - if mode & S_IRWXO: - STATE.ERRORS += 1 - print _('$dbdir "other" perms must be 000'), - if STATE.FIX: - print _('(fixing)') - os.chmod(dbdir, mode & ~S_IRWXO) - else: - print - - - -def checkcgi(): - cgidir = os.path.join(config.EXEC_PREFIX, 'cgi-bin') - if STATE.VERBOSE: - print _('checking cgi-bin permissions') - exes = os.listdir(cgidir) - for f in exes: - path = os.path.join(cgidir, f) - if STATE.VERBOSE: - print _(' checking set-gid for $path') - mode = statmode(path) - if mode & S_IXGRP and not mode & S_ISGID: - STATE.ERRORS += 1 - print _('$path must be set-gid'), - if STATE.FIX: - print _('(fixing)') - os.chmod(path, mode | S_ISGID) - else: - print - - - -def checkmail(): - wrapper = os.path.join(config.WRAPPER_DIR, 'mailman') - if STATE.VERBOSE: - print _('checking set-gid for $wrapper') - mode = statmode(wrapper) - if not mode & S_ISGID: - STATE.ERRORS += 1 - print _('$wrapper must be set-gid'), - if STATE.FIX: - print _('(fixing)') - os.chmod(wrapper, mode | S_ISGID) - - - -def checkadminpw(): - for pwfile in (os.path.join(config.DATA_DIR, 'adm.pw'), - os.path.join(config.DATA_DIR, 'creator.pw')): - targetmode = S_IFREG | S_IRUSR | S_IWUSR | S_IRGRP - if STATE.VERBOSE: - print _('checking permissions on $pwfile') - try: - mode = statmode(pwfile) - except OSError, e: - if e.errno <> errno.ENOENT: - raise - return - if mode <> targetmode: - STATE.ERRORS += 1 - octmode = oct(mode) - print _('$pwfile permissions must be exactly 0640 (got $octmode)'), - if STATE.FIX: - print _('(fixing)') - os.chmod(pwfile, targetmode) - else: - print - - -def checkmta(): - if config.MTA: - modname = 'mailman.MTA.' + config.MTA - __import__(modname) - try: - sys.modules[modname].checkperms(STATE) - except AttributeError: - pass - - - -def checkdata(): - targetmode = S_IRUSR | S_IWUSR | S_IRGRP | S_IWGRP - checkfiles = ('config.pck', 'config.pck.last', - 'config.db', 'config.db.last', - 'next-digest', 'next-digest-topics', - 'digest.mbox', 'pending.pck', - 'request.db', 'request.db.tmp') - if STATE.VERBOSE: - print _('checking permissions on list data') - for dir in os.listdir(config.LIST_DATA_DIR): - for file in checkfiles: - path = os.path.join(config.LIST_DATA_DIR, dir, file) - if STATE.VERBOSE: - print _(' checking permissions on: $path') - try: - mode = statmode(path) - except OSError, e: - if e.errno <> errno.ENOENT: - raise - continue - if (mode & targetmode) <> targetmode: - STATE.ERRORS += 1 - print _('file permissions must be at least 660: $path'), - if STATE.FIX: - print _('(fixing)') - os.chmod(path, mode | targetmode) - else: - print - - - -def parseargs(): - parser = optparse.OptionParser(version=MAILMAN_VERSION, - usage=_("""\ -%prog [options] - -Check the permissions of all Mailman files. With no options, just report the -permission and ownership problems found.""")) - parser.add_option('-f', '--fix', - default=False, action='store_true', help=_("""\ -Fix all permission and ownership problems found. With this option, you must -run check_perms as root.""")) - parser.add_option('-v', '--verbose', - default=False, action='store_true', - help=_('Produce more verbose output')) - parser.add_option('-C', '--config', - help=_('Alternative configuration file to use')) - opts, args = parser.parse_args() - if args: - parser.print_help() - print >> sys.stderr, _('Unexpected arguments') - sys.exit(1) - return parser, opts, args - - - -def main(): - global MAILMAN_USER, MAILMAN_GROUP, MAILMAN_UID, MAILMAN_GID - - parser, opts, args = parseargs() - STATE.FIX = opts.fix - STATE.VERBOSE = opts.verbose - - config.load(opts.config) - - MAILMAN_USER = config.MAILMAN_USER - MAILMAN_GROUP = config.MAILMAN_GROUP - # Let KeyErrors percolate - MAILMAN_GID = grp.getgrnam(MAILMAN_GROUP).gr_gid - MAILMAN_UID = pwd.getpwnam(MAILMAN_USER).pw_uid - - checkall() - checkarchives() - checkarchivedbs() - checkcgi() - checkmail() - checkdata() - checkadminpw() - checkmta() - - if not STATE.ERRORS: - print _('No problems found') - else: - print _('Problems found:'), STATE.ERRORS - print _('Re-run as $MAILMAN_USER (or root) with -f flag to fix') - - -if __name__ == '__main__': - main() diff --git a/mailman/bin/checkdbs.py b/mailman/bin/checkdbs.py deleted file mode 100644 index 2ce08aab7..000000000 --- a/mailman/bin/checkdbs.py +++ /dev/null @@ -1,199 +0,0 @@ -# Copyright (C) 1998-2009 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 . - -import sys -import time -import optparse - -from email.Charset import Charset - -from mailman import MailList -from mailman import Message -from mailman import Utils -from mailman import i18n -from mailman.app.requests import handle_request -from mailman.configuration import config -from mailman.version import MAILMAN_VERSION - -_ = i18n._ - -# Work around known problems with some RedHat cron daemons -import signal -signal.signal(signal.SIGCHLD, signal.SIG_DFL) - -NL = u'\n' -now = time.time() - - - -def parseargs(): - parser = optparse.OptionParser(version=MAILMAN_VERSION, - usage=_("""\ -%prog [options] - -Check for pending admin requests and mail the list owners if necessary.""")) - parser.add_option('-C', '--config', - help=_('Alternative configuration file to use')) - opts, args = parser.parse_args() - if args: - parser.print_help() - print >> sys.stderr, _('Unexpected arguments') - sys.exit(1) - return opts, args, parser - - - -def pending_requests(mlist): - # Must return a byte string - lcset = Utils.GetCharSet(mlist.preferred_language) - pending = [] - first = True - requestsdb = config.db.get_list_requests(mlist) - for request in requestsdb.of_type(RequestType.subscription): - if first: - pending.append(_('Pending subscriptions:')) - first = False - key, data = requestsdb.get_request(request.id) - when = data['when'] - addr = data['addr'] - fullname = data['fullname'] - passwd = data['passwd'] - digest = data['digest'] - lang = data['lang'] - if fullname: - if isinstance(fullname, unicode): - fullname = fullname.encode(lcset, 'replace') - fullname = ' (%s)' % fullname - pending.append(' %s%s %s' % (addr, fullname, time.ctime(when))) - first = True - for request in requestsdb.of_type(RequestType.held_message): - if first: - pending.append(_('\nPending posts:')) - first = False - key, data = requestsdb.get_request(request.id) - when = data['when'] - sender = data['sender'] - subject = data['subject'] - reason = data['reason'] - text = data['text'] - msgdata = data['msgdata'] - subject = Utils.oneline(subject, lcset) - date = time.ctime(when) - reason = _(reason) - pending.append(_("""\ -From: $sender on $date -Subject: $subject -Cause: $reason""")) - pending.append('') - # Coerce all items in pending to a Unicode so we can join them - upending = [] - charset = Utils.GetCharSet(mlist.preferred_language) - for s in pending: - if isinstance(s, unicode): - upending.append(s) - else: - upending.append(unicode(s, charset, 'replace')) - # Make sure that the text we return from here can be encoded to a byte - # string in the charset of the list's language. This could fail if for - # example, the request was pended while the list's language was French, - # but then it was changed to English before checkdbs ran. - text = NL.join(upending) - charset = Charset(Utils.GetCharSet(mlist.preferred_language)) - incodec = charset.input_codec or 'ascii' - outcodec = charset.output_codec or 'ascii' - if isinstance(text, unicode): - return text.encode(outcodec, 'replace') - # Be sure this is a byte string encodeable in the list's charset - utext = unicode(text, incodec, 'replace') - return utext.encode(outcodec, 'replace') - - - -def auto_discard(mlist): - # Discard old held messages - discard_count = 0 - expire = config.days(mlist.max_days_to_hold) - requestsdb = config.db.get_list_requests(mlist) - heldmsgs = list(requestsdb.of_type(RequestType.held_message)) - if expire and heldmsgs: - for request in heldmsgs: - key, data = requestsdb.get_request(request.id) - if now - data['date'] > expire: - handle_request(mlist, request.id, config.DISCARD) - discard_count += 1 - mlist.Save() - return discard_count - - - -def main(): - opts, args, parser = parseargs() - config.load(opts.config) - - i18n.set_language(config.DEFAULT_SERVER_LANGUAGE) - - for name in config.list_manager.names: - # The list must be locked in order to open the requests database - mlist = MailList.MailList(name) - try: - count = config.db.requests.get_list_requests(mlist).count - # While we're at it, let's evict yesterday's autoresponse data - midnight_today = Utils.midnight() - evictions = [] - for sender in mlist.hold_and_cmd_autoresponses.keys(): - date, respcount = mlist.hold_and_cmd_autoresponses[sender] - if Utils.midnight(date) < midnight_today: - evictions.append(sender) - if evictions: - for sender in evictions: - del mlist.hold_and_cmd_autoresponses[sender] - # This is the only place we've changed the list's database - mlist.Save() - if count: - i18n.set_language(mlist.preferred_language) - realname = mlist.real_name - discarded = auto_discard(mlist) - if discarded: - count = count - discarded - text = _( - 'Notice: $discarded old request(s) automatically expired.\n\n') - else: - text = '' - if count: - text += Utils.maketext( - 'checkdbs.txt', - {'count' : count, - 'host_name': mlist.host_name, - 'adminDB' : mlist.GetScriptURL('admindb', absolute=1), - 'real_name': realname, - }, mlist=mlist) - text += '\n' + pending_requests(mlist) - subject = _('$count $realname moderator request(s) waiting') - else: - subject = _('$realname moderator request check result') - msg = Message.UserNotification(mlist.GetOwnerEmail(), - mlist.GetBouncesEmail(), - subject, text, - mlist.preferred_language) - msg.send(mlist, **{'tomoderators': True}) - finally: - mlist.Unlock() - - - -if __name__ == '__main__': - main() diff --git a/mailman/bin/cleanarch.py b/mailman/bin/cleanarch.py deleted file mode 100644 index 325fad91a..000000000 --- a/mailman/bin/cleanarch.py +++ /dev/null @@ -1,133 +0,0 @@ -# Copyright (C) 2001-2009 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 . - -"""Clean up an .mbox archive file.""" - -import re -import sys -import mailbox -import optparse - -from mailman.i18n import _ -from mailman.version import MAILMAN_VERSION - - -cre = re.compile(mailbox.UnixMailbox._fromlinepattern) -# From RFC 2822, a header field name must contain only characters from 33-126 -# inclusive, excluding colon. I.e. from oct 41 to oct 176 less oct 072. Must -# use re.match() so that it's anchored at the beginning of the line. -fre = re.compile(r'[\041-\071\073-\176]+') - - - -def parseargs(): - parser = optparse.OptionParser(version=MAILMAN_VERSION, - usage=_("""\ -%prog [options] < inputfile > outputfile - -The archiver looks for Unix-From lines separating messages in an mbox archive -file. For compatibility, it specifically looks for lines that start with -'From ' -- i.e. the letters capital-F, lowercase-r, o, m, space, ignoring -everything else on the line. - -Normally, any lines that start 'From ' in the body of a message should be -escaped such that a > character is actually the first on a line. It is -possible though that body lines are not actually escaped. This script -attempts to fix these by doing a stricter test of the Unix-From lines. Any -lines that start From ' but do not pass this stricter test are escaped with a -'>' character.""")) - parser.add_option('-q', '--quiet', - default=False, action='store_true', help=_("""\ -Don't print changed line information to standard error.""")) - parser.add_option('-s', '--status', - default=-1, type='int', help=_("""\ -Print a '#' character for every n lines processed. With a number less than or -equal to zero, suppress the '#' characters.""")) - parser.add_option('-n', '--dry-run', - default=False, action='store_true', help=_("""\ -Don't actually output anything.""")) - opts, args = parser.parser_args() - if args: - parser.print_error(_('Unexpected arguments')) - return parser, opts, args - - - -def escape_line(line, lineno, quiet, output): - if output: - sys.stdout.write('>' + line) - if not quiet: - print >> sys.stderr, _('Unix-From line changed: $lineno') - print >> sys.stderr, line[:-1] - - - -def main(): - parser, opts, args = parseargs() - - lineno = 0 - statuscnt = 0 - messages = 0 - prevline = None - while True: - lineno += 1 - line = sys.stdin.readline() - if not line: - break - if line.startswith('From '): - if cre.match(line): - # This is a real Unix-From line. But it could be a message - # /about/ Unix-From lines, so as a second order test, make - # sure there's at least one RFC 2822 header following - nextline = sys.stdin.readline() - lineno += 1 - if not nextline: - # It was the last line of the mbox, so it couldn't have - # been a Unix-From - escape_line(line, lineno, quiet, output) - break - fieldname = nextline.split(':', 1) - if len(fieldname) < 2 or not fre.match(nextline): - # The following line was not a header, so this wasn't a - # valid Unix-From - escape_line(line, lineno, quiet, output) - if output: - sys.stdout.write(nextline) - else: - # It's a valid Unix-From line - messages += 1 - if output: - # Before we spit out the From_ line, make sure the - # previous line was blank. - if prevline is not None and prevline <> '\n': - sys.stdout.write('\n') - sys.stdout.write(line) - sys.stdout.write(nextline) - else: - # This is a bogus Unix-From line - escape_line(line, lineno, quiet, output) - elif output: - # Any old line - sys.stdout.write(line) - if status > 0 and (lineno % status) == 0: - sys.stderr.write('#') - statuscnt += 1 - if statuscnt > 50: - print >> sys.stderr - statuscnt = 0 - prevline = line - print >> sys.stderr, _('%(messages)d messages found') diff --git a/mailman/bin/config_list.py b/mailman/bin/config_list.py deleted file mode 100644 index a5cec9480..000000000 --- a/mailman/bin/config_list.py +++ /dev/null @@ -1,332 +0,0 @@ -# Copyright (C) 1998-2009 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 . - -import re -import sys -import time -import optparse - -from mailman import errors -from mailman import MailList -from mailman import Utils -from mailman import i18n -from mailman.configuration import config -from mailman.version import MAILMAN_VERSION - -_ = i18n._ - -NL = '\n' -nonasciipat = re.compile(r'[\x80-\xff]') - - - -def parseargs(): - parser = optparse.OptionParser(version=MAILMAN_VERSION, - usage=_("""\ -%prog [options] listname - -Configure a list from a text file description, or dump a list's configuration -settings.""")) - parser.add_option('-i', '--inputfile', - metavar='FILENAME', default=None, type='string', - help=_("""\ -Configure the list by assigning each module-global variable in the file to an -attribute on the mailing list object, then save the list. The named file is -loaded with execfile() and must be legal Python code. Any variable that isn't -already an attribute of the list object is ignored (a warning message is -printed). See also the -c option. - -A special variable named 'mlist' is put into the globals during the execfile, -which is bound to the actual MailList object. This lets you do all manner of -bizarre thing to the list object, but BEWARE! Using this can severely (and -possibly irreparably) damage your mailing list! - -The may not be used with the -o option.""")) - parser.add_option('-o', '--outputfile', - metavar='FILENAME', default=None, type='string', - help=_("""\ -Instead of configuring the list, print out a mailing list's configuration -variables in a format suitable for input using this script. In this way, you -can easily capture the configuration settings for a particular list and -imprint those settings on another list. FILENAME is the file to output the -settings to. If FILENAME is `-', standard out is used. - -This may not be used with the -i option.""")) - parser.add_option('-c', '--checkonly', - default=False, action='store_true', help=_("""\ -With this option, the modified list is not actually changed. This is only -useful with the -i option.""")) - parser.add_option('-v', '--verbose', - default=False, action='store_true', help=_("""\ -Print the name of each attribute as it is being changed. This is only useful -with the -i option.""")) - parser.add_option('-C', '--config', - help=_('Alternative configuration file to use')) - opts, args = parser.parse_args() - if len(args) > 1: - parser.print_help() - parser.error(_('Unexpected arguments')) - if not args: - parser.error(_('List name is required')) - return parser, opts, args - - - -def do_output(listname, outfile, parser): - closep = False - try: - if outfile == '-': - outfp = sys.stdout - else: - outfp = open(outfile, 'w') - closep = True - # Open the specified list unlocked, since we're only reading it. - try: - mlist = MailList.MailList(listname, lock=False) - except errors.MMListError: - parser.error(_('No such list: $listname')) - # Preamble for the config info. PEP 263 charset and capture time. - language = mlist.preferred_language - charset = Utils.GetCharSet(language) - i18n.set_language(language) - if not charset: - charset = 'us-ascii' - when = time.ctime(time.time()) - print >> outfp, _('''\ -# -*- python -*- -# -*- coding: $charset -*- -## "$listname" mailing list configuration settings -## captured on $when -''') - # Get all the list config info. All this stuff is accessible via the - # web interface. - for k in config.ADMIN_CATEGORIES: - subcats = mlist.GetConfigSubCategories(k) - if subcats is None: - do_list_categories(mlist, k, None, outfp) - else: - for subcat in [t[0] for t in subcats]: - do_list_categories(mlist, k, subcat, outfp) - finally: - if closep: - outfp.close() - - - -def do_list_categories(mlist, k, subcat, outfp): - info = mlist.GetConfigInfo(k, subcat) - label, gui = mlist.GetConfigCategories()[k] - if info is None: - return - charset = Utils.GetCharSet(mlist.preferred_language) - print >> outfp, '##', k.capitalize(), _('options') - print >> outfp, '#' - # First, massage the descripton text, which could have obnoxious - # leading whitespace on second and subsequent lines due to - # triple-quoted string nonsense in the source code. - desc = NL.join([s.lstrip() for s in info[0].splitlines()]) - # Print out the category description - desc = Utils.wrap(desc) - for line in desc.splitlines(): - print >> outfp, '#', line - print >> outfp - for data in info[1:]: - if not isinstance(data, tuple): - continue - varname = data[0] - # Variable could be volatile - if varname[0] == '_': - continue - vtype = data[1] - # First, massage the descripton text, which could have - # obnoxious leading whitespace on second and subsequent lines - # due to triple-quoted string nonsense in the source code. - desc = NL.join([s.lstrip() for s in data[-1].splitlines()]) - # Now strip out all HTML tags - desc = re.sub('<.*?>', '', desc) - # And convert </> to <> - desc = re.sub('<', '<', desc) - desc = re.sub('>', '>', desc) - # Print out the variable description. - desc = Utils.wrap(desc) - for line in desc.split('\n'): - print >> outfp, '#', line - # munge the value based on its type - value = None - if hasattr(gui, 'getValue'): - value = gui.getValue(mlist, vtype, varname, data[2]) - if value is None and not varname.startswith('_'): - value = getattr(mlist, varname) - if vtype in (config.String, config.Text, config.FileUpload): - print >> outfp, varname, '=', - lines = value.splitlines() - if not lines: - print >> outfp, "''" - elif len(lines) == 1: - if charset <> 'us-ascii' and nonasciipat.search(lines[0]): - # This is more readable for non-english list. - print >> outfp, '"' + lines[0].replace('"', '\\"') + '"' - else: - print >> outfp, repr(lines[0]) - else: - if charset == 'us-ascii' and nonasciipat.search(value): - # Normally, an english list should not have non-ascii char. - print >> outfp, repr(NL.join(lines)) - else: - outfp.write(' """') - outfp.write(NL.join(lines).replace('"', '\\"')) - outfp.write('"""\n') - elif vtype in (config.Radio, config.Toggle): - print >> outfp, '#' - print >> outfp, '#', _('legal values are:') - # TBD: This is disgusting, but it's special cased - # everywhere else anyway... - if varname == 'subscribe_policy' and \ - not config.ALLOW_OPEN_SUBSCRIBE: - i = 1 - else: - i = 0 - for choice in data[2]: - print >> outfp, '# ', i, '= "%s"' % choice - i += 1 - print >> outfp, varname, '=', repr(value) - else: - print >> outfp, varname, '=', repr(value) - print >> outfp - - - -def getPropertyMap(mlist): - guibyprop = {} - categories = mlist.GetConfigCategories() - for category, (label, gui) in categories.items(): - if not hasattr(gui, 'GetConfigInfo'): - continue - subcats = mlist.GetConfigSubCategories(category) - if subcats is None: - subcats = [(None, None)] - for subcat, sclabel in subcats: - for element in gui.GetConfigInfo(mlist, category, subcat): - if not isinstance(element, tuple): - continue - propname = element[0] - wtype = element[1] - guibyprop[propname] = (gui, wtype) - return guibyprop - - -class FakeDoc: - # Fake the error reporting API for the htmlformat.Document class - def addError(self, s, tag=None, *args): - if tag: - print >> sys.stderr, tag - print >> sys.stderr, s % args - - def set_language(self, val): - pass - - - -def do_input(listname, infile, checkonly, verbose, parser): - fakedoc = FakeDoc() - # Open the specified list locked, unless checkonly is set - try: - mlist = MailList.MailList(listname, lock=not checkonly) - except errors.MMListError, e: - parser.error(_('No such list "$listname"\n$e')) - savelist = False - guibyprop = getPropertyMap(mlist) - try: - globals = {'mlist': mlist} - # Any exception that occurs in execfile() will cause the list to not - # be saved, but any other problems are not save-fatal. - execfile(infile, globals) - savelist = True - for k, v in globals.items(): - if k in ('mlist', '__builtins__'): - continue - if not hasattr(mlist, k): - print >> sys.stderr, _('attribute "$k" ignored') - continue - if verbose: - print >> sys.stderr, _('attribute "$k" changed') - missing = [] - gui, wtype = guibyprop.get(k, (missing, missing)) - if gui is missing: - # This isn't an official property of the list, but that's - # okay, we'll just restore it the old fashioned way - print >> sys.stderr, _('Non-standard property restored: $k') - setattr(mlist, k, v) - else: - # BAW: This uses non-public methods. This logic taken from - # the guts of GUIBase.handleForm(). - try: - validval = gui._getValidValue(mlist, k, wtype, v) - except ValueError: - print >> sys.stderr, _('Invalid value for property: $k') - except errors.EmailAddressError: - print >> sys.stderr, _( - 'Bad email address for option $k: $v') - else: - # BAW: Horrible hack, but then this is special cased - # everywhere anyway. :( Privacy._setValue() knows that - # when ALLOW_OPEN_SUBSCRIBE is false, the web values are - # 0, 1, 2 but these really should be 1, 2, 3, so it adds - # one. But we really do provide [0..3] so we need to undo - # the hack that _setValue adds. :( :( - if k == 'subscribe_policy' and \ - not config.ALLOW_OPEN_SUBSCRIBE: - validval -= 1 - # BAW: Another horrible hack. This one is just too hard - # to fix in a principled way in Mailman 2.1 - elif k == 'new_member_options': - # Because this is a Checkbox, _getValidValue() - # transforms the value into a list of one item. - validval = validval[0] - validval = [bitfield for bitfield, bitval - in config.OPTINFO.items() - if validval & bitval] - gui._setValue(mlist, k, validval, fakedoc) - # BAW: when to do gui._postValidate()??? - finally: - if savelist and not checkonly: - mlist.Save() - mlist.Unlock() - - - -def main(): - parser, opts, args = parseargs() - config.load(opts.config) - listname = args[0] - - # Sanity check - if opts.inputfile and opts.outputfile: - parser.error(_('Only one of -i or -o is allowed')) - if not opts.inputfile and not opts.outputfile: - parser.error(_('One of -i or -o is required')) - - if opts.outputfile: - do_output(listname, opts.outputfile, parser) - else: - do_input(listname, opts.inputfile, opts.checkonly, - opts.verbose, parser) - - - -if __name__ == '__main__': - main() diff --git a/mailman/bin/create_list.py b/mailman/bin/create_list.py deleted file mode 100644 index 8058a7d67..000000000 --- a/mailman/bin/create_list.py +++ /dev/null @@ -1,129 +0,0 @@ -# Copyright (C) 1998-2009 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 . - -import sys - -from mailman import Message -from mailman import Utils -from mailman import i18n -from mailman.app.lifecycle import create_list -from mailman.config import config -from mailman.core import errors -from mailman.interfaces.listmanager import ListAlreadyExistsError -from mailman.options import SingleMailingListOptions - - -_ = i18n._ - - - -class ScriptOptions(SingleMailingListOptions): - usage = _("""\ -%prog [options] - -Create a new mailing list. - -fqdn_listname is the 'fully qualified list name', basically the posting -address of the list. It must be a valid email address and the domain must be -registered with Mailman. - -Note that listnames are forced to lowercase.""") - - def add_options(self): - super(ScriptOptions, self).add_options() - self.parser.add_option( - '--language', - type='unicode', action='store', - help=_("""\ -Make the list's preferred language LANGUAGE, which must be a two letter -language code.""")) - self.parser.add_option( - '-o', '--owner', - type='unicode', action='append', default=[], - dest='owners', help=_("""\ -Specific a listowner email address. If the address is not currently -registered with Mailman, the address is registered and linked to a user. -Mailman will send a confirmation message to the address, but it will also send -a list creation notice to the address. More than one owner can be -specified.""")) - self.parser.add_option( - '-q', '--quiet', - default=False, action='store_true', - help=_("""\ -Normally the administrator is notified by email (after a prompt) that their -list has been created. This option suppresses the prompt and -notification.""")) - self.parser.add_option( - '-a', '--automate', - default=False, action='store_true', - help=_("""\ -This option suppresses the prompt prior to administrator notification but -still sends the notification. It can be used to make newlist totally -non-interactive but still send the notification, assuming at least one list -owner is specified with the -o option..""")) - - def sanity_check(self): - """Set up some defaults we couldn't set up earlier.""" - if self.options.language is None: - self.options.language = unicode(config.mailman.default_language) - # Is the language known? - if self.options.language not in config.languages.enabled_codes: - self.parser.error(_('Unknown language: $opts.language')) - # Handle variable number of positional arguments - if len(self.arguments) > 0: - parser.error(_('Unexpected arguments')) - - - -def main(): - options = ScriptOptions() - options.initialize() - - # Create the mailing list, applying styles as appropriate. - fqdn_listname = options.options.listname - if fqdn_listname is None: - options.parser.error(_('--listname is required')) - try: - mlist = create_list(fqdn_listname, options.options.owners) - mlist.preferred_language = options.options.language - except errors.InvalidEmailAddress: - options.parser.error(_('Illegal list name: $fqdn_listname')) - except ListAlreadyExistsError: - options.parser.error(_('List already exists: $fqdn_listname')) - except errors.BadDomainSpecificationError, domain: - options.parser.error(_('Undefined domain: $domain')) - - config.db.commit() - - if not options.options.quiet: - d = dict( - listname = mlist.fqdn_listname, - admin_url = mlist.script_url('admin'), - listinfo_url = mlist.script_url('listinfo'), - requestaddr = mlist.request_address, - siteowner = mlist.no_reply_address, - ) - text = Utils.maketext('newlist.txt', d, mlist=mlist) - # Set the I18N language to the list's preferred language so the header - # will match the template language. Stashing and restoring the old - # translation context is just (healthy? :) paranoia. - with i18n.using_language(mlist.preferred_language): - msg = Message.UserNotification( - owner_mail, mlist.no_reply_address, - _('Your new mailing list: $fqdn_listname'), - text, mlist.preferred_language) - msg.send(mlist) diff --git a/mailman/bin/disabled.py b/mailman/bin/disabled.py deleted file mode 100644 index cc8eb2c69..000000000 --- a/mailman/bin/disabled.py +++ /dev/null @@ -1,201 +0,0 @@ -# Copyright (C) 2001-2009 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 . - -import time -import logging -import optparse - -from mailman import errors -from mailman import MailList -from mailman import MemberAdaptor -from mailman import Pending -from mailman import loginit -from mailman.Bouncer import _BounceInfo -from mailman.configuration import config -from mailman.i18n import _ -from mailman.version import MAILMAN_VERSION - - -# Work around known problems with some RedHat cron daemons -import signal -signal.signal(signal.SIGCHLD, signal.SIG_DFL) - -ALL = (MemberAdaptor.BYBOUNCE, - MemberAdaptor.BYADMIN, - MemberAdaptor.BYUSER, - MemberAdaptor.UNKNOWN, - ) - - - -def who_callback(option, opt, value, parser): - dest = getattr(parser.values, option.dest) - if opt in ('-o', '--byadmin'): - dest.add(MemberAdaptor.BYADMIN) - elif opt in ('-m', '--byuser'): - dest.add(MemberAdaptor.BYUSER) - elif opt in ('-u', '--unknown'): - dest.add(MemberAdaptor.UNKNOWN) - elif opt in ('-b', '--notbybounce'): - dest.discard(MemberAdaptor.BYBOUNCE) - elif opt in ('-a', '--all'): - dest.update(ALL) - - -def parseargs(): - parser = optparse.OptionParser(version=MAILMAN_VERSION, - usage=_("""\ -%prog [options] - -Process disabled members, recommended once per day. - -This script iterates through every mailing list looking for members whose -delivery is disabled. If they have been disabled due to bounces, they will -receive another notification, or they may be removed if they've received the -maximum number of notifications. - -Use the --byadmin, --byuser, and --unknown flags to also send notifications to -members whose accounts have been disabled for those reasons. Use --all to -send the notification to all disabled members.""")) - # This is the set of working flags for who to send notifications to. By - # default, we notify anybody who has been disable due to bounces. - parser.set_defaults(who=set([MemberAdaptor.BYBOUNCE])) - parser.add_option('-o', '--byadmin', - callback=who_callback, action='callback', dest='who', - help=_("""\ -Also send notifications to any member disabled by the list -owner/administrator.""")) - parser.add_option('-m', '--byuser', - callback=who_callback, action='callback', dest='who', - help=_("""\ -Also send notifications to any member who has disabled themself.""")) - parser.add_option('-u', '--unknown', - callback=who_callback, action='callback', dest='who', - help=_("""\ -Also send notifications to any member disabled for unknown reasons -(usually a legacy disabled address).""")) - parser.add_option('-b', '--notbybounce', - callback=who_callback, action='callback', dest='who', - help=_("""\ -Don't send notifications to members disabled because of bounces (the -default is to notify bounce disabled members).""")) - parser.add_option('-a', '--all', - callback=who_callback, action='callback', dest='who', - help=_('Send notifications to all disabled members')) - parser.add_option('-f', '--force', - default=False, action='store_true', - help=_("""\ -Send notifications to disabled members even if they're not due a new -notification yet.""")) - parser.add_option('-l', '--listname', - dest='listnames', action='append', default=[], - type='string', help=_("""\ -Process only the given list, otherwise do all lists.""")) - parser.add_option('-C', '--config', - help=_('Alternative configuration file to use')) - opts, args = parser.parse_args() - return opts, args, parser - - - -def main(): - opts, args, parser = parseargs() - config.load(opts.config) - - loginit.initialize(propagate=True) - elog = logging.getLogger('mailman.error') - blog = logging.getLogger('mailman.bounce') - - listnames = set(opts.listnames or config.list_manager.names) - who = tuple(opts.who) - - msg = _('[disabled by periodic sweep and cull, no message available]') - today = time.mktime(time.localtime()[:3] + (0,) * 6) - for listname in listnames: - # List of members to notify - notify = [] - mlist = MailList.MailList(listname) - try: - interval = mlist.bounce_you_are_disabled_warnings_interval - # Find all the members who are currently bouncing and see if - # they've reached the disable threshold but haven't yet been - # disabled. This is a sweep through the membership catching - # situations where they've bounced a bunch, then the list admin - # lowered the threshold, but we haven't (yet) seen more bounces - # from the member. Note: we won't worry about stale information - # or anything else since the normal bounce processing code will - # handle that. - disables = [] - for member in mlist.getBouncingMembers(): - if mlist.getDeliveryStatus(member) <> MemberAdaptor.ENABLED: - continue - info = mlist.getBounceInfo(member) - if info.score >= mlist.bounce_score_threshold: - disables.append((member, info)) - if disables: - for member, info in disables: - mlist.disableBouncingMember(member, info, msg) - # Go through all the members who have delivery disabled, and find - # those that are due to have another notification. If they are - # disabled for another reason than bouncing, and we're processing - # them (because of the command line switch) then they won't have a - # bounce info record. We can piggyback on that for all disable - # purposes. - members = mlist.getDeliveryStatusMembers(who) - for member in members: - info = mlist.getBounceInfo(member) - if not info: - # See if they are bounce disabled, or disabled for some - # other reason. - status = mlist.getDeliveryStatus(member) - if status == MemberAdaptor.BYBOUNCE: - elog.error( - '%s disabled BYBOUNCE lacks bounce info, list: %s', - member, mlist.internal_name()) - continue - info = _BounceInfo( - member, 0, today, - mlist.bounce_you_are_disabled_warnings, - mlist.pend_new(Pending.RE_ENABLE, - mlist.internal_name(), - member)) - mlist.setBounceInfo(member, info) - lastnotice = time.mktime(info.lastnotice + (0,) * 6) - if opts.force or today >= lastnotice + interval: - notify.append(member) - # Now, send notifications to anyone who is due - for member in notify: - blog.info('Notifying disabled member %s for list: %s', - member, mlist.internal_name()) - try: - mlist.sendNextNotification(member) - except errors.NotAMemberError: - # There must have been some problem with the data we have - # on this member. Most likely it's that they don't have a - # password assigned. Log this and delete the member. - blog.info( - 'NotAMemberError when sending disabled notice: %s', - member) - mlist.ApprovedDeleteMember(member, 'cron/disabled') - mlist.Save() - finally: - mlist.Unlock() - - - -if __name__ == '__main__': - main() diff --git a/mailman/bin/docs/master.txt b/mailman/bin/docs/master.txt deleted file mode 100644 index 0d3cade77..000000000 --- a/mailman/bin/docs/master.txt +++ /dev/null @@ -1,49 +0,0 @@ -Mailman queue runner control -============================ - -Mailman has a number of queue runners which process messages in its queue file -directories. In normal operation, a command line script called 'mailmanctl' -is used to start, stop and manage the queue runners. mailmanctl actually is -just a wrapper around the real queue runner watcher script called master.py. - - >>> from mailman.testing.helpers import TestableMaster - -Start the master in a subthread. - - >>> master = TestableMaster() - >>> master.start() - -There should be a process id for every qrunner that claims to be startable. - - >>> from lazr.config import as_boolean - >>> startable_qrunners = [qconf for qconf in config.qrunner_configs - ... if as_boolean(qconf.start)] - >>> len(list(master.qrunner_pids)) == len(startable_qrunners) - True - -Now verify that all the qrunners are running. - - >>> import os - - # This should produce no output. - >>> for pid in master.qrunner_pids: - ... os.kill(pid, 0) - -Stop the master process, which should also kill (and not restart) the child -queue runner processes. - - >>> master.stop() - -None of the children are running now. - - >>> import errno - >>> for pid in master.qrunner_pids: - ... try: - ... os.kill(pid, 0) - ... print 'Process did not exit:', pid - ... except OSError, error: - ... if error.errno == errno.ESRCH: - ... # The child process exited. - ... pass - ... else: - ... raise diff --git a/mailman/bin/dumpdb.py b/mailman/bin/dumpdb.py deleted file mode 100644 index 6657602e4..000000000 --- a/mailman/bin/dumpdb.py +++ /dev/null @@ -1,88 +0,0 @@ -# Copyright (C) 1998-2009 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 . - -import pprint -import cPickle - -from mailman.config import config -from mailman.i18n import _ -from mailman.interact import interact -from mailman.options import Options - - -COMMASPACE = ', ' -m = [] - - - -class ScriptOptions(Options): - usage=_("""\ -%prog [options] filename - -Dump the contents of any Mailman queue file. The queue file is a data file -with multiple Python pickles in it.""") - - def add_options(self): - super(ScriptOptions, self).add_options() - self.parser.add_option( - '-n', '--noprint', - dest='doprint', default=True, action='store_false', - help=_("""\ -Don't attempt to pretty print the object. This is useful if there is some -problem with the object and you just want to get an unpickled representation. -Useful with 'bin/dumpdb -i '. In that case, the list of -unpickled objects will be left in a variable called 'm'.""")) - self.parser.add_option( - '-i', '--interact', - default=False, action='store_true', - help=_("""\ -Start an interactive Python session, with a variable called 'm' containing the -list of unpickled objects.""")) - - def sanity_check(self): - if len(self.arguments) < 1: - self.parser.error(_('No filename given.')) - elif len(self.arguments) > 1: - self.parser.error(_('Unexpected arguments')) - else: - self.filename = self.arguments[0] - - - -def main(): - options = ScriptOptions() - options.initialize() - - pp = pprint.PrettyPrinter(indent=4) - with open(options.filename) as fp: - while True: - try: - m.append(cPickle.load(fp)) - except EOFError: - break - if options.options.doprint: - print _('[----- start pickle -----]') - for i, obj in enumerate(m): - count = i + 1 - print _('<----- start object $count ----->') - if isinstance(obj, basestring): - print obj - else: - pp.pprint(obj) - print _('[----- end pickle -----]') - if options.options.interact: - interact() diff --git a/mailman/bin/export.py b/mailman/bin/export.py deleted file mode 100644 index d1992b4b4..000000000 --- a/mailman/bin/export.py +++ /dev/null @@ -1,310 +0,0 @@ -# Copyright (C) 2006-2009 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 . - -"""Export an XML representation of a mailing list.""" - -import sys -import codecs -import datetime -import optparse - -from xml.sax.saxutils import escape - -from mailman import Defaults -from mailman import errors -from mailman import MemberAdaptor -from mailman.MailList import MailList -from mailman.configuration import config -from mailman.i18n import _ -from mailman.initialize import initialize -from mailman.version import MAILMAN_VERSION - - -SPACE = ' ' - -TYPES = { - Defaults.Toggle : 'bool', - Defaults.Radio : 'radio', - Defaults.String : 'string', - Defaults.Text : 'text', - Defaults.Email : 'email', - Defaults.EmailList : 'email_list', - Defaults.Host : 'host', - Defaults.Number : 'number', - Defaults.FileUpload : 'upload', - Defaults.Select : 'select', - Defaults.Topics : 'topics', - Defaults.Checkbox : 'checkbox', - Defaults.EmailListEx : 'email_list_ex', - Defaults.HeaderFilter : 'header_filter', - } - - - -class Indenter: - def __init__(self, fp, indentwidth=4): - self._fp = fp - self._indent = 0 - self._width = indentwidth - - def indent(self): - self._indent += 1 - - def dedent(self): - self._indent -= 1 - assert self._indent >= 0 - - def write(self, s): - if s <> '\n': - self._fp.write(self._indent * self._width * ' ') - self._fp.write(s) - - - -class XMLDumper(object): - def __init__(self, fp): - self._fp = Indenter(fp) - self._tagbuffer = None - self._stack = [] - - def _makeattrs(self, tagattrs): - # The attribute values might contain angle brackets. They might also - # be None. - attrs = [] - for k, v in tagattrs.items(): - if v is None: - v = '' - else: - v = escape(str(v)) - attrs.append('%s="%s"' % (k, v)) - return SPACE.join(attrs) - - def _flush(self, more=True): - if not self._tagbuffer: - return - name, attributes = self._tagbuffer - self._tagbuffer = None - if attributes: - attrstr = ' ' + self._makeattrs(attributes) - else: - attrstr = '' - if more: - print >> self._fp, '<%s%s>' % (name, attrstr) - self._fp.indent() - self._stack.append(name) - else: - print >> self._fp, '<%s%s/>' % (name, attrstr) - - # Use this method when you know you have sub-elements. - def _push_element(self, _name, **_tagattrs): - self._flush() - self._tagbuffer = (_name, _tagattrs) - - def _pop_element(self, _name): - buffered = bool(self._tagbuffer) - self._flush(more=False) - if not buffered: - name = self._stack.pop() - assert name == _name, 'got: %s, expected: %s' % (_name, name) - self._fp.dedent() - print >> self._fp, '' % name - - # Use this method when you do not have sub-elements - def _element(self, _name, _value=None, **_attributes): - self._flush() - if _attributes: - attrs = ' ' + self._makeattrs(_attributes) - else: - attrs = '' - if _value is None: - print >> self._fp, '<%s%s/>' % (_name, attrs) - else: - # The value might contain angle brackets. - value = escape(unicode(_value)) - print >> self._fp, '<%s%s>%s' % (_name, attrs, value, _name) - - def _do_list_categories(self, mlist, k, subcat=None): - info = mlist.GetConfigInfo(k, subcat) - label, gui = mlist.GetConfigCategories()[k] - if info is None: - return - for data in info[1:]: - if not isinstance(data, tuple): - continue - varname = data[0] - # Variable could be volatile - if varname.startswith('_'): - continue - vtype = data[1] - # Munge the value based on its type - value = None - if hasattr(gui, 'getValue'): - value = gui.getValue(mlist, vtype, varname, data[2]) - if value is None: - value = getattr(mlist, varname) - widget_type = TYPES[vtype] - if isinstance(value, list): - self._push_element('option', name=varname, type=widget_type) - for v in value: - self._element('value', v) - self._pop_element('option') - else: - self._element('option', value, name=varname, type=widget_type) - - def _dump_list(self, mlist): - # Write list configuration values - self._push_element('list', name=mlist.fqdn_listname) - self._push_element('configuration') - self._element('option', - mlist.preferred_language, - name='preferred_language') - for k in config.ADMIN_CATEGORIES: - subcats = mlist.GetConfigSubCategories(k) - if subcats is None: - self._do_list_categories(mlist, k) - else: - for subcat in [t[0] for t in subcats]: - self._do_list_categories(mlist, k, subcat) - self._pop_element('configuration') - # Write membership - self._push_element('roster') - digesters = set(mlist.getDigestMemberKeys()) - for member in sorted(mlist.getMembers()): - attrs = dict(id=member) - cased = mlist.getMemberCPAddress(member) - if cased <> member: - attrs['original'] = cased - self._push_element('member', **attrs) - self._element('realname', mlist.getMemberName(member)) - self._element('password', mlist.getMemberPassword(member)) - self._element('language', mlist.getMemberLanguage(member)) - # Delivery status, combined with the type of delivery - attrs = {} - status = mlist.getDeliveryStatus(member) - if status == MemberAdaptor.ENABLED: - attrs['status'] = 'enabled' - else: - attrs['status'] = 'disabled' - attrs['reason'] = {MemberAdaptor.BYUSER : 'byuser', - MemberAdaptor.BYADMIN : 'byadmin', - MemberAdaptor.BYBOUNCE : 'bybounce', - }.get(mlist.getDeliveryStatus(member), - 'unknown') - if member in digesters: - if mlist.getMemberOption(member, Defaults.DisableMime): - attrs['delivery'] = 'plain' - else: - attrs['delivery'] = 'mime' - else: - attrs['delivery'] = 'regular' - changed = mlist.getDeliveryStatusChangeTime(member) - if changed: - when = datetime.datetime.fromtimestamp(changed) - attrs['changed'] = when.isoformat() - self._element('delivery', **attrs) - for option, flag in Defaults.OPTINFO.items(): - # Digest/Regular delivery flag must be handled separately - if option in ('digest', 'plain'): - continue - value = mlist.getMemberOption(member, flag) - self._element(option, value) - topics = mlist.getMemberTopics(member) - if not topics: - self._element('topics') - else: - self._push_element('topics') - for topic in topics: - self._element('topic', topic) - self._pop_element('topics') - self._pop_element('member') - self._pop_element('roster') - self._pop_element('list') - - def dump(self, listnames): - print >> self._fp, '' - self._push_element('mailman', **{ - 'xmlns:xsi': 'http://www.w3.org/2001/XMLSchema-instance', - 'xsi:noNamespaceSchemaLocation': 'ssi-1.0.xsd', - }) - for listname in sorted(listnames): - try: - mlist = MailList(listname, lock=False) - except errors.MMUnknownListError: - print >> sys.stderr, _('No such list: $listname') - continue - self._dump_list(mlist) - self._pop_element('mailman') - - def close(self): - while self._stack: - self._pop_element() - - - -def parseargs(): - parser = optparse.OptionParser(version=MAILMAN_VERSION, - usage=_("""\ -%prog [options] - -Export the configuration and members of a mailing list in XML format.""")) - parser.add_option('-o', '--outputfile', - metavar='FILENAME', default=None, type='string', - help=_("""\ -Output XML to FILENAME. If not given, or if FILENAME is '-', standard out is -used.""")) - parser.add_option('-l', '--listname', - default=[], action='append', type='string', - metavar='LISTNAME', dest='listnames', help=_("""\ -The list to include in the output. If not given, then all mailing lists are -included in the XML output. Multiple -l flags may be given.""")) - parser.add_option('-C', '--config', - help=_('Alternative configuration file to use')) - opts, args = parser.parse_args() - if args: - parser.print_help() - parser.error(_('Unexpected arguments')) - return parser, opts, args - - - -def main(): - parser, opts, args = parseargs() - initialize(opts.config) - - close = False - if opts.outputfile in (None, '-'): - writer = codecs.getwriter('utf-8') - fp = writer(sys.stdout) - else: - fp = codecs.open(opts.outputfile, 'w', 'utf-8') - close = True - - try: - dumper = XMLDumper(fp) - if opts.listnames: - listnames = [] - for listname in opts.listnames: - if '@' not in listname: - listname = '%s@%s' % (listname, config.DEFAULT_EMAIL_HOST) - listnames.append(listname) - else: - listnames = config.list_manager.names - dumper.dump(listnames) - dumper.close() - finally: - if close: - fp.close() diff --git a/mailman/bin/find_member.py b/mailman/bin/find_member.py deleted file mode 100644 index 0982724a0..000000000 --- a/mailman/bin/find_member.py +++ /dev/null @@ -1,135 +0,0 @@ -# Copyright (C) 1998-2009 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 . - -import re -import sys -import optparse - -from mailman import errors -from mailman import MailList -from mailman.configuration import config -from mailman.i18n import _ -from mailman.version import MAILMAN_VERSION - - -AS_MEMBER = 0x01 -AS_OWNER = 0x02 - - - -def parseargs(): - parser = optparse.OptionParser(version=MAILMAN_VERSION, - usage=_("""\ -%prog [options] regex [regex ...] - -Find all lists that a member's address is on. - -The interaction between -l and -x (see below) is as follows. If any -l option -is given then only the named list will be included in the search. If any -x -option is given but no -l option is given, then all lists will be search -except those specifically excluded. - -Regular expression syntax uses the Python 're' module. Complete -specifications are at: - -http://www.python.org/doc/current/lib/module-re.html - -Address matches are case-insensitive, but case-preserved addresses are -displayed.""")) - parser.add_option('-l', '--listname', - type='string', default=[], action='append', - dest='listnames', - help=_('Include only the named list in the search')) - parser.add_option('-x', '--exclude', - type='string', default=[], action='append', - dest='excludes', - help=_('Exclude the named list from the search')) - parser.add_option('-w', '--owners', - default=False, action='store_true', - help=_('Search list owners as well as members')) - parser.add_option('-C', '--config', - help=_('Alternative configuration file to use')) - opts, args = parser.parse_args() - if not args: - parser.print_help() - print >> sys.stderr, _('Search regular expression required') - sys.exit(1) - return parser, opts, args - - - -def main(): - parser, opts, args = parseargs() - config.load(opts.config) - - listnames = opts.listnames or config.list_manager.names - includes = set(listname.lower() for listname in listnames) - excludes = set(listname.lower() for listname in opts.excludes) - listnames = includes - excludes - - if not listnames: - print _('No lists to search') - return - - cres = [] - for r in args: - cres.append(re.compile(r, re.IGNORECASE)) - # dictionary of {address, (listname, ownerp)} - matches = {} - for listname in listnames: - try: - mlist = MailList.MailList(listname, lock=False) - except errors.MMListError: - print _('No such list: $listname') - continue - if opts.owners: - owners = mlist.owner - else: - owners = [] - for cre in cres: - for member in mlist.getMembers(): - if cre.search(member): - addr = mlist.getMemberCPAddress(member) - entries = matches.get(addr, {}) - aswhat = entries.get(listname, 0) - aswhat |= AS_MEMBER - entries[listname] = aswhat - matches[addr] = entries - for owner in owners: - if cre.search(owner): - entries = matches.get(owner, {}) - aswhat = entries.get(listname, 0) - aswhat |= AS_OWNER - entries[listname] = aswhat - matches[owner] = entries - addrs = matches.keys() - addrs.sort() - for k in addrs: - hits = matches[k] - lists = hits.keys() - print k, _('found in:') - for name in lists: - aswhat = hits[name] - if aswhat & AS_MEMBER: - print ' ', name - if aswhat & AS_OWNER: - print ' ', name, _('(as owner)') - - - -if __name__ == '__main__': - main() diff --git a/mailman/bin/gate_news.py b/mailman/bin/gate_news.py deleted file mode 100644 index eac30422d..000000000 --- a/mailman/bin/gate_news.py +++ /dev/null @@ -1,243 +0,0 @@ -# Copyright (C) 1998-2009 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 . - -import os -import sys -import time -import socket -import logging -import nntplib -import optparse -import email.Errors - -from email.Parser import Parser -from locknix import lockfile - -from mailman import MailList -from mailman import Message -from mailman import Utils -from mailman import loginit -from mailman.configuration import config -from mailman.i18n import _ -from mailman.queue import Switchboard -from mailman.version import MAILMAN_VERSION - -# Work around known problems with some RedHat cron daemons -import signal -signal.signal(signal.SIGCHLD, signal.SIG_DFL) - -NL = '\n' - -log = None - -class _ContinueLoop(Exception): - pass - - - -def parseargs(): - parser = optparse.OptionParser(version=MAILMAN_VERSION, - usage=_("""\ -%prog [options] - -Poll the NNTP servers for messages to be gatewayed to mailing lists.""")) - parser.add_option('-C', '--config', - help=_('Alternative configuration file to use')) - opts, args = parser.parse_args() - if args: - parser.print_help() - print >> sys.stderr, _('Unexpected arguments') - sys.exit(1) - return opts, args, parser - - - -_hostcache = {} - -def open_newsgroup(mlist): - # Split host:port if given - nntp_host, nntp_port = Utils.nntpsplit(mlist.nntp_host) - # Open up a "mode reader" connection to nntp server. This will be shared - # for all the gated lists having the same nntp_host. - conn = _hostcache.get(mlist.nntp_host) - if conn is None: - try: - conn = nntplib.NNTP(nntp_host, nntp_port, - readermode=True, - user=config.NNTP_USERNAME, - password=config.NNTP_PASSWORD) - except (socket.error, nntplib.NNTPError, IOError), e: - log.error('error opening connection to nntp_host: %s\n%s', - mlist.nntp_host, e) - raise - _hostcache[mlist.nntp_host] = conn - # Get the GROUP information for the list, but we're only really interested - # in the first article number and the last article number - r, c, f, l, n = conn.group(mlist.linked_newsgroup) - return conn, int(f), int(l) - - -def clearcache(): - for conn in set(_hostcache.values()): - conn.quit() - _hostcache.clear() - - - -# This function requires the list to be locked. -def poll_newsgroup(mlist, conn, first, last, glock): - listname = mlist.internal_name() - # NEWNEWS is not portable and has synchronization issues. - for num in range(first, last): - glock.refresh() - try: - headers = conn.head(repr(num))[3] - found_to = False - beenthere = False - for header in headers: - i = header.find(':') - value = header[:i].lower() - if i > 0 and value == 'to': - found_to = True - if value <> 'x-beenthere': - continue - if header[i:] == ': %s' % mlist.posting_address: - beenthere = True - break - if not beenthere: - body = conn.body(repr(num))[3] - # Usenet originated messages will not have a Unix envelope - # (i.e. "From " header). This breaks Pipermail archiving, so - # we will synthesize one. Be sure to use the format searched - # for by mailbox.UnixMailbox._isrealfromline(). BAW: We use - # the -bounces address here in case any downstream clients use - # the envelope sender for bounces; I'm not sure about this, - # but it's the closest to the old semantics. - lines = ['From %s %s' % (mlist.GetBouncesEmail(), - time.ctime(time.time()))] - lines.extend(headers) - lines.append('') - lines.extend(body) - lines.append('') - p = Parser(Message.Message) - try: - msg = p.parsestr(NL.join(lines)) - except email.Errors.MessageError, e: - log.error('email package exception for %s:%d\n%s', - mlist.linked_newsgroup, num, e) - raise _ContinueLoop - if found_to: - del msg['X-Originally-To'] - msg['X-Originally-To'] = msg['To'] - del msg['To'] - msg['To'] = mlist.posting_address - # Post the message to the locked list - inq = Switchboard(config.INQUEUE_DIR) - inq.enqueue(msg, - listname=mlist.internal_name(), - fromusenet=True) - log.info('posted to list %s: %7d', listname, num) - except nntplib.NNTPError, e: - log.exception('NNTP error for list %s: %7d', listname, num) - except _ContinueLoop: - continue - # Even if we don't post the message because it was seen on the - # list already, update the watermark - mlist.usenet_watermark = num - - - -def process_lists(glock): - for listname in config.list_manager.names: - glock.refresh() - # Open the list unlocked just to check to see if it is gating news to - # mail. If not, we're done with the list. Otherwise, lock the list - # and gate the group. - mlist = MailList.MailList(listname, lock=False) - if not mlist.gateway_to_mail: - continue - # Get the list's watermark, i.e. the last article number that we gated - # from news to mail. None means that this list has never polled its - # newsgroup and that we should do a catch up. - watermark = getattr(mlist, 'usenet_watermark', None) - # Open the newsgroup, but let most exceptions percolate up. - try: - conn, first, last = open_newsgroup(mlist) - except (socket.error, nntplib.NNTPError): - break - log.info('%s: [%d..%d]', listname, first, last) - try: - try: - if watermark is None: - mlist.Lock(timeout=config.LIST_LOCK_TIMEOUT) - # This is the first time we've tried to gate this - # newsgroup. We essentially do a mass catch-up, otherwise - # we'd flood the mailing list. - mlist.usenet_watermark = last - log.info('%s caught up to article %d', listname, last) - else: - # The list has been polled previously, so now we simply - # grab all the messages on the newsgroup that have not - # been seen by the mailing list. The first such article - # is the maximum of the lowest article available in the - # newsgroup and the watermark. It's possible that some - # articles have been expired since the last time gate_news - # has run. Not much we can do about that. - start = max(watermark + 1, first) - if start > last: - log.info('nothing new for list %s', listname) - else: - mlist.Lock(timeout=config.LIST_LOCK_TIMEOUT) - log.info('gating %s articles [%d..%d]', - listname, start, last) - # Use last+1 because poll_newsgroup() employes a for - # loop over range, and this will not include the last - # element in the list. - poll_newsgroup(mlist, conn, start, last + 1, glock) - except lockfile.TimeOutError: - log.error('Could not acquire list lock: %s', listname) - finally: - if mlist.Locked(): - mlist.Save() - mlist.Unlock() - log.info('%s watermark: %d', listname, mlist.usenet_watermark) - - - -def main(): - opts, args, parser = parseargs() - config.load(opts.config) - - GATENEWS_LOCK_FILE = os.path.join(config.LOCK_DIR, 'gate_news.lock') - LOCK_LIFETIME = config.hours(2) - - loginit.initialize(propagate=True) - log = logging.getLogger('mailman.fromusenet') - - try: - with lockfile.Lock(GATENEWS_LOCK_FILE, - # It's okay to hijack this - lifetime=LOCK_LIFETIME) as lock: - process_lists(lock) - clearcache() - except lockfile.TimeOutError: - log.error('Could not acquire gate_news lock') - - - -if __name__ == '__main__': - main() diff --git a/mailman/bin/genaliases.py b/mailman/bin/genaliases.py deleted file mode 100644 index e8916d030..000000000 --- a/mailman/bin/genaliases.py +++ /dev/null @@ -1,64 +0,0 @@ -# Copyright (C) 2001-2009 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 . - -__metaclass__ = type -__all__ = [ - 'main', - ] - - -import sys - -from mailman.config import config -from mailman.i18n import _ -from mailman.options import Options - - - -class ScriptOptions(Options): - """Options for the genaliases script.""" - - usage = _("""\ -%prog [options] - -Regenerate the Mailman specific MTA aliases from scratch. The actual output -depends on the value of the 'MTA' variable in your etc/mailman.cfg file.""") - - def add_options(self): - super(ScriptOptions, self).add_options() - self.parser.add_option( - '-q', '--quiet', - default=False, action='store_true', help=_("""\ -Some MTA output can include more verbose help text. Use this to tone down the -verbosity.""")) - - - - -def main(): - options = ScriptOptions() - options.initialize() - - # Get the MTA-specific module. - module_path, class_path = config.mta.incoming.rsplit('.', 1) - __import__(module_path) - getattr(sys.modules[module_path], class_path)().regenerate() - - - -if __name__ == '__main__': - main() diff --git a/mailman/bin/import.py b/mailman/bin/import.py deleted file mode 100644 index d2361e808..000000000 --- a/mailman/bin/import.py +++ /dev/null @@ -1,315 +0,0 @@ -# Copyright (C) 2006-2009 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 . - -"""Import the XML representation of a mailing list.""" - -import sys -import codecs -import optparse -import traceback - -from xml.dom import minidom -from xml.parsers.expat import ExpatError - -from mailman import Defaults -from mailman import errors -from mailman import MemberAdaptor -from mailman import Utils -from mailman import passwords -from mailman.MailList import MailList -from mailman.i18n import _ -from mailman.initialize import initialize -from mailman.version import MAILMAN_VERSION - - -OPTS = None - - - -def nodetext(node): - # Expect only one TEXT_NODE in the list of children - for child in node.childNodes: - if child.nodeType == node.TEXT_NODE: - return child.data - return u'' - - -def nodegen(node, *elements): - for child in node.childNodes: - if child.nodeType <> minidom.Node.ELEMENT_NODE: - continue - if elements and child.tagName not in elements: - print _('Ignoring unexpected element: $node.tagName') - else: - yield child - - - -def parse_config(node): - config = dict() - for child in nodegen(node, 'option'): - name = child.getAttribute('name') - if not name: - print _('Skipping unnamed option') - continue - vtype = child.getAttribute('type') or 'string' - if vtype in ('email_list', 'email_list_ex', 'checkbox'): - value = [] - for subnode in nodegen(child): - value.append(nodetext(subnode)) - elif vtype == 'bool': - value = nodetext(child) - try: - value = bool(int(value)) - except ValueError: - value = {'true' : True, - 'false': False, - }.get(value.lower()) - if value is None: - print _('Skipping bad boolean value: $value') - continue - elif vtype == 'radio': - value = nodetext(child).lower() - boolval = {'true' : True, - 'false': False, - }.get(value) - if boolval is None: - value = int(value) - else: - value = boolval - elif vtype == 'number': - value = nodetext(child) - # First try int then float - try: - value = int(value) - except ValueError: - value = float(value) - elif vtype in ('header_filter', 'topics'): - value = [] - fakebltins = dict(__builtins__ = dict(True=True, False=False)) - for subnode in nodegen(child): - reprstr = nodetext(subnode) - # Turn the reprs back into tuples, in a safe way - tupleval = eval(reprstr, fakebltins) - value.append(tupleval) - else: - value = nodetext(child) - # And now some special casing :( - if name == 'new_member_options': - value = int(nodetext(child)) - config[name] = value - return config - - - - -def parse_roster(node): - members = [] - for child in nodegen(node, 'member'): - member = dict() - member['id'] = mid = child.getAttribute('id') - if not mid: - print _('Skipping member with no id') - continue - if OPTS.verbose: - print _('* Processing member: $mid') - for subnode in nodegen(child): - attr = subnode.tagName - if attr == 'delivery': - value = (subnode.getAttribute('status'), - subnode.getAttribute('delivery')) - elif attr in ('hide', 'ack', 'notmetoo', 'nodupes', 'nomail'): - value = {'true' : True, - 'false': False, - }.get(nodetext(subnode).lower(), False) - elif attr == 'topics': - value = [] - for subsubnode in nodegen(subnode): - value.append(nodetext(subsubnode)) - elif attr == 'password': - value = nodetext(subnode) - if OPTS.reset_passwords or value == '{NONE}' or not value: - value = passwords.make_secret(Utils.MakeRandomPassword()) - else: - value = nodetext(subnode) - member[attr] = value - members.append(member) - return members - - - -def load(fp): - try: - doc = minidom.parse(fp) - except ExpatError: - print _('Expat error in file: $fp.name') - traceback.print_exc() - sys.exit(1) - doc.normalize() - # Make sure there's only one top-level node - gen = nodegen(doc, 'mailman') - top = gen.next() - try: - gen.next() - except StopIteration: - pass - else: - print _('Malformed XML; duplicate nodes') - sys.exit(1) - all_listdata = [] - for listnode in nodegen(top, 'list'): - listdata = dict() - name = listnode.getAttribute('name') - if OPTS.verbose: - print _('Processing list: $name') - if not name: - print _('Ignoring malformed node') - continue - for child in nodegen(listnode, 'configuration', 'roster'): - if child.tagName == 'configuration': - list_config = parse_config(child) - else: - assert(child.tagName == 'roster') - list_roster = parse_roster(child) - all_listdata.append((name, list_config, list_roster)) - return all_listdata - - - -def create(all_listdata): - for name, list_config, list_roster in all_listdata: - fqdn_listname = '%s@%s' % (name, list_config['host_name']) - if Utils.list_exists(fqdn_listname): - print _('Skipping already existing list: $fqdn_listname') - continue - mlist = MailList() - try: - if OPTS.verbose: - print _('Creating mailing list: $fqdn_listname') - mlist.Create(fqdn_listname, list_config['owner'][0], - list_config['password']) - except errors.BadDomainSpecificationError: - print _('List is not in a supported domain: $fqdn_listname') - continue - # Save the list creation, then unlock and relock the list. This is so - # that we use normal SQLAlchemy transactions to manage all the - # attribute and membership updates. Without this, no transaction will - # get committed in the second Save() below and we'll lose all our - # updates. - mlist.Save() - mlist.Unlock() - mlist.Lock() - try: - for option, value in list_config.items(): - # XXX Here's what sucks. Some properties need to have - # _setValue() called on the gui component, because those - # methods do some pre-processing on the values before they're - # applied to the MailList instance. But we don't have a good - # way to find a category and sub-category that a particular - # property belongs to. Plus this will probably change. So - # for now, we'll just hard code the extra post-processing - # here. The good news is that not all _setValue() munging - # needs to be done -- for example, we've already converted - # everything to dollar strings. - if option in ('filter_mime_types', 'pass_mime_types', - 'filter_filename_extensions', - 'pass_filename_extensions'): - value = value.splitlines() - if option == 'available_languages': - mlist.set_languages(*value) - else: - setattr(mlist, option, value) - for member in list_roster: - mid = member['id'] - if OPTS.verbose: - print _('* Adding member: $mid') - status, delivery = member['delivery'] - kws = {'password' : member['password'], - 'language' : member['language'], - 'realname' : member['realname'], - 'digest' : delivery <> 'regular', - } - mlist.addNewMember(mid, **kws) - status = {'enabled' : MemberAdaptor.ENABLED, - 'byuser' : MemberAdaptor.BYUSER, - 'byadmin' : MemberAdaptor.BYADMIN, - 'bybounce' : MemberAdaptor.BYBOUNCE, - }.get(status, MemberAdaptor.UNKNOWN) - mlist.setDeliveryStatus(mid, status) - for opt in ('hide', 'ack', 'notmetoo', 'nodupes', 'nomail'): - mlist.setMemberOption(mid, - Defaults.OPTINFO[opt], - member[opt]) - topics = member.get('topics') - if topics: - mlist.setMemberTopics(mid, topics) - mlist.Save() - finally: - mlist.Unlock() - - - -def parseargs(): - parser = optparse.OptionParser(version=MAILMAN_VERSION, - usage=_("""\ -%prog [options] - -Import the configuration and/or members of a mailing list in XML format. The -imported mailing list must not already exist. All mailing lists named in the -XML file are imported, but those that already exist are skipped unless --error -is given.""")) - parser.add_option('-i', '--inputfile', - metavar='FILENAME', default=None, type='string', - help=_("""\ -Input XML from FILENAME. If not given, or if FILENAME is '-', standard input -is used.""")) - parser.add_option('-p', '--reset-passwords', - default=False, action='store_true', help=_("""\ -With this option, user passwords in the XML are ignored and are reset to a -random password. If the generated passwords were not included in the input -XML, they will always be randomly generated.""")) - parser.add_option('-v', '--verbose', - default=False, action='store_true', - help=_('Produce more verbose output')) - parser.add_option('-C', '--config', - help=_('Alternative configuration file to use')) - opts, args = parser.parse_args() - if args: - parser.print_help() - parser.error(_('Unexpected arguments')) - return parser, opts, args - - - -def main(): - global OPTS - - parser, opts, args = parseargs() - initialize(opts.config) - OPTS = opts - - if opts.inputfile in (None, '-'): - fp = sys.stdin - else: - fp = open(opts.inputfile, 'r') - - try: - listbags = load(fp) - create(listbags) - finally: - if fp is not sys.stdin: - fp.close() diff --git a/mailman/bin/inject.py b/mailman/bin/inject.py deleted file mode 100644 index 2bc8a49e3..000000000 --- a/mailman/bin/inject.py +++ /dev/null @@ -1,89 +0,0 @@ -# Copyright (C) 2002-2009 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 . - -import os -import sys - -from email import message_from_string - -from mailman import Utils -from mailman.Message import Message -from mailman.configuration import config -from mailman.i18n import _ -from mailman.inject import inject_text -from mailman.options import SingleMailingListOptions - - - -class ScriptOptions(SingleMailingListOptions): - usage=_("""\ -%prog [options] [filename] - -Inject a message from a file into Mailman's incoming queue. 'filename' is the -name of the plaintext message file to inject. If omitted, or the string '-', -standard input is used. -""") - - def add_options(self): - super(ScriptOptions, self).add_options() - self.parser.add_option( - '-q', '--queue', - type='string', help=_("""\ -The name of the queue to inject the message to. The queuename must be one of -the directories inside the qfiles directory. If omitted, the incoming queue -is used.""")) - - def sanity_check(self): - if not self.options.listname: - self.parser.error(_('Missing listname')) - if len(self.arguments) == 0: - self.filename = '-' - elif len(self.arguments) > 1: - self.parser.print_error(_('Unexpected arguments')) - else: - self.filename = self.arguments[0] - - - -def main(): - options = ScriptOptions() - options.initialize() - - if options.options.queue is None: - qdir = config.INQUEUE_DIR - else: - qdir = os.path.join(config.QUEUE_DIR, options.options.queue) - if not os.path.isdir(qdir): - options.parser.error(_('Bad queue directory: $qdir')) - - fqdn_listname = options.options.listname - mlist = config.db.list_manager.get(fqdn_listname) - if mlist is None: - options.parser.error(_('No such list: $fqdn_listname')) - - if options.filename == '-': - message_text = sys.stdin.read() - else: - with open(options.filename) as fp: - message_text = fp.read() - - inject_text(mlist, message_text, qdir=qdir) - - - -if __name__ == '__main__': - main() diff --git a/mailman/bin/list_lists.py b/mailman/bin/list_lists.py deleted file mode 100644 index ea1640910..000000000 --- a/mailman/bin/list_lists.py +++ /dev/null @@ -1,104 +0,0 @@ -# Copyright (C) 1998-2009 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 . - -from mailman.config import config -from mailman.i18n import _ -from mailman.options import Options - - - -class ScriptOptions(Options): - usage = _("""\ -%prog [options] - -List all mailing lists.""") - - def add_options(self): - super(ScriptOptions, self).add_options() - self.parser.add_option( - '-a', '--advertised', - default=False, action='store_true', - help=_("""\ -List only those mailing lists that are publicly advertised""")) - self.parser.add_option( - '-b', '--bare', - default=False, action='store_true', - help=_("""\ -Displays only the list name, with no description.""")) - self.parser.add_option( - '-d', '--domain', - default=[], type='string', action='append', - dest='domains', help=_("""\ -List only those mailing lists that match the given virtual domain, which may -be either the email host or the url host name. Multiple -d options may be -given.""")) - self.parser.add_option( - '-f', '--full', - default=False, action='store_true', - help=_("""\ -Print the full list name, including the posting address.""")) - - def sanity_check(self): - if len(self.arguments) > 0: - self.parser.error(_('Unexpected arguments')) - - - -def main(): - options = ScriptOptions() - options.initialize() - - mlists = [] - longest = 0 - - listmgr = config.db.list_manager - for fqdn_name in sorted(listmgr.names): - mlist = listmgr.get(fqdn_name) - if options.options.advertised and not mlist.advertised: - continue - if options.options.domains: - for domain in options.options.domains: - if domain in mlist.web_page_url or domain == mlist.host_name: - mlists.append(mlist) - break - else: - mlists.append(mlist) - if options.options.full: - name = mlist.fqdn_listname - else: - name = mlist.real_name - longest = max(len(name), longest) - - if not mlists and not options.options.bare: - print _('No matching mailing lists found') - return - - if not options.options.bare: - num_mlists = len(mlists) - print _('$num_mlists matching mailing lists found:') - - format = '%%%ds - %%.%ds' % (longest, 77 - longest) - for mlist in mlists: - if options.options.full: - name = mlist.fqdn_listname - else: - name = mlist.real_name - if options.options.bare: - print name - else: - description = mlist.description or _('[no description available]') - print ' ', format % (name, description) diff --git a/mailman/bin/list_members.py b/mailman/bin/list_members.py deleted file mode 100644 index 443f764d6..000000000 --- a/mailman/bin/list_members.py +++ /dev/null @@ -1,201 +0,0 @@ -# Copyright (C) 1998-2009 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 . - -import sys - -from email.Utils import formataddr - -from mailman import Utils -from mailman.config import config -from mailman.core import errors -from mailman.i18n import _ -from mailman.interfaces import DeliveryStatus -from mailman.options import SingleMailingListOptions - - -COMMASPACE = ', ' - -WHYCHOICES = { - 'enabled' : DeliveryStatus.enabled, - 'byuser' : DeliveryStatus.by_user, - 'byadmin' : DeliveryStatus.by_moderator, - 'bybounce': DeliveryStatus.by_bounces, - } - -KINDCHOICES = set(('mime', 'plain', 'any')) - - - -class ScriptOptions(SingleMailingListOptions): - usage = _("""\ -%prog [options] - -List all the members of a mailing list. Note that with the options below, if -neither -r or -d is supplied, regular members are printed first, followed by -digest members, but no indication is given as to address status. - -listname is the name of the mailing list to use.""") - - def add_options(self): - super(ScriptOptions, self).add_options() - self.parser.add_option( - '-o', '--output', - type='string', help=_("""\ -Write output to specified file instead of standard out.""")) - self.parser.add_option( - '-r', '--regular', - default=None, action='store_true', - help=_('Print just the regular (non-digest) members.')) - self.parser.add_option( - '-d', '--digest', - default=None, type='string', metavar='KIND', - help=_("""\ -Print just the digest members. KIND can be 'mime', 'plain', or -'any'. 'mime' prints just the members receiving MIME digests, while 'plain' -prints just the members receiving plain text digests. 'any' prints all -members receiving any kind of digest.""")) - self.parser.add_option( - '-n', '--nomail', - type='string', metavar='WHY', help=_("""\ -Print the members that have delivery disabled. WHY selects just the subset of -members with delivery disabled for a particular reason, where 'any' prints all -disabled members. 'byadmin', 'byuser', 'bybounce', and 'unknown' prints just -the users who are disabled for that particular reason. WHY can also be -'enabled' which prints just those members for whom delivery is enabled.""")) - self.parser.add_option( - '-f', '--fullnames', - default=False, action='store_true', - help=_('Include the full names in the output')) - self.parser.add_option( - '-i', '--invalid', - default=False, action='store_true', help=_("""\ -Print only the addresses in the membership list that are invalid. Ignores -r, --d, -n.""")) - - def sanity_check(self): - if not self.options.listname: - self.parser.error(_('Missing listname')) - if len(self.arguments) > 0: - self.parser.print_error(_('Unexpected arguments')) - if self.options.digest is not None: - self.options.kind = self.options.digest.lower() - if self.options.kind not in KINDCHOICES: - self.parser.error( - _('Invalid value for -d: $self.options.digest')) - if self.options.nomail is not None: - why = self.options.nomail.lower() - if why == 'any': - self.options.why = 'any' - elif why not in WHYCHOICES: - self.parser.error( - _('Invalid value for -n: $self.options.nomail')) - self.options.why = why - if self.options.regular is None and self.options.digest is None: - self.options.regular = self.options.digest = True - self.options.kind = 'any' - - - -def safe(string): - if not string: - return '' - return string.encode(sys.getdefaultencoding(), 'replace') - - -def isinvalid(addr): - try: - Utils.ValidateEmail(addr) - return False - except errors.EmailAddressError: - return True - - - -def whymatches(mlist, addr, why): - # Return true if the `why' matches the reason the address is enabled, or - # in the case of why is None, that they are disabled for any reason - # (i.e. not enabled). - status = mlist.getDeliveryStatus(addr) - if why in (None, 'any'): - return status <> DeliveryStatus.enabled - return status == WHYCHOICES[why] - - - -def main(): - options = ScriptOptions() - options.initialize() - - fqdn_listname = options.options.listname - if options.options.output: - try: - fp = open(options.output, 'w') - except IOError: - options.parser.error( - _('Could not open file for writing: $options.options.output')) - else: - fp = sys.stdout - - mlist = config.db.list_manager.get(fqdn_listname) - if mlist is None: - options.parser.error(_('No such list: $fqdn_listname')) - - # The regular delivery and digest members. - rmembers = set(mlist.regular_members.members) - dmembers = set(mlist.digest_members.members) - - fullnames = options.options.fullnames - if options.options.invalid: - all = sorted(member.address.address for member in rmembers + dmembers) - for address in all: - user = config.db.user_manager.get_user(address) - name = (user.real_name if fullnames and user else u'') - if options.options.invalid and isinvalid(address): - print >> fp, formataddr((safe(name), address)) - return - if options.options.regular: - for address in sorted(member.address.address for member in rmembers): - user = config.db.user_manager.get_user(address) - name = (user.real_name if fullnames and user else u'') - # Filter out nomails - if (options.options.nomail and - not whymatches(mlist, address, options.options.why)): - continue - print >> fp, formataddr((safe(name), address)) - if options.options.digest: - for address in sorted(member.address.address for member in dmembers): - user = config.db.user_manager.get_user(address) - name = (user.real_name if fullnames and user else u'') - # Filter out nomails - if (options.options.nomail and - not whymatches(mlist, address, options.options.why)): - continue - # Filter out digest kinds -## if mlist.getMemberOption(addr, config.DisableMime): -## # They're getting plain text digests -## if opts.kind == 'mime': -## continue -## else: -## # They're getting MIME digests -## if opts.kind == 'plain': -## continue - print >> fp, formataddr((safe(name), address)) - - - -if __name__ == '__main__': - main() diff --git a/mailman/bin/list_owners.py b/mailman/bin/list_owners.py deleted file mode 100644 index 953fb8941..000000000 --- a/mailman/bin/list_owners.py +++ /dev/null @@ -1,88 +0,0 @@ -# Copyright (C) 2002-2009 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 . - -import sys -import optparse - -from mailman.MailList import MailList -from mailman.configuration import config -from mailman.i18n import _ -from mailman.initialize import initialize -from mailman.version import MAILMAN_VERSION - - - -def parseargs(): - parser = optparse.OptionParser(version=MAILMAN_VERSION, - usage=_("""\ -%prog [options] [listname ...] - -List the owners of a mailing list, or all mailing lists if no list names are -given.""")) - parser.add_option('-w', '--with-listnames', - default=False, action='store_true', - help=_("""\ -Group the owners by list names and include the list names in the output. -Otherwise, the owners will be sorted and uniquified based on the email -address.""")) - parser.add_option('-m', '--moderators', - default=False, action='store_true', - help=_('Include the list moderators in the output.')) - parser.add_option('-C', '--config', - help=_('Alternative configuration file to use')) - opts, args = parser.parse_args() - return parser, opts, args - - - -def main(): - parser, opts, args = parseargs() - initialize(opts.config) - - listmgr = config.db.list_manager - listnames = set(args or listmgr.names) - bylist = {} - - for listname in listnames: - mlist = listmgr.get(listname) - addrs = [addr.address for addr in mlist.owners.addresses] - if opts.moderators: - addrs.extend([addr.address for addr in mlist.moderators.addresses]) - bylist[listname] = addrs - - if opts.with_listnames: - for listname in listnames: - unique = set() - for addr in bylist[listname]: - unique.add(addr) - keys = list(unique) - keys.sort() - print listname - for k in keys: - print '\t', k - else: - unique = set() - for listname in listnames: - for addr in bylist[listname]: - unique.add(addr) - for k in sorted(unique): - print k - - - -if __name__ == '__main__': - main() diff --git a/mailman/bin/mailmanctl.py b/mailman/bin/mailmanctl.py deleted file mode 100644 index 667a46a70..000000000 --- a/mailman/bin/mailmanctl.py +++ /dev/null @@ -1,232 +0,0 @@ -# Copyright (C) 2001-2009 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 . - -"""Mailman start/stop script.""" - -import os -import grp -import pwd -import sys -import errno -import signal -import logging - -from optparse import OptionParser - -from mailman.config import config -from mailman.core.initialize import initialize -from mailman.i18n import _ -from mailman.version import MAILMAN_VERSION - - -COMMASPACE = ', ' - -log = None -parser = None - - - -def parseargs(): - parser = OptionParser(version=MAILMAN_VERSION, - usage=_("""\ -Primary start-up and shutdown script for Mailman's qrunner daemon. - -This script starts, stops, and restarts the main Mailman queue runners, making -sure that the various long-running qrunners are still alive and kicking. It -does this by forking and exec'ing the qrunners and waiting on their pids. -When it detects a subprocess has exited, it may restart it. - -The qrunners respond to SIGINT, SIGTERM, SIGUSR1 and SIGHUP. SIGINT, SIGTERM -and SIGUSR1 all cause the qrunners to exit cleanly, but the master will only -restart qrunners that have exited due to a SIGUSR1. SIGHUP causes the master -and the qrunners to close their log files, and reopen then upon the next -printed message. - -The master also responds to SIGINT, SIGTERM, SIGUSR1 and SIGHUP, which it -simply passes on to the qrunners (note that the master will close and reopen -its own log files on receipt of a SIGHUP). The master also leaves its own -process id in the file data/master-qrunner.pid but you normally don't need to -use this pid directly. The `start', `stop', `restart', and `reopen' commands -handle everything for you. - -Commands: - - start - Start the master daemon and all qrunners. Prints a message and - exits if the master daemon is already running. - - stop - Stops the master daemon and all qrunners. After stopping, no - more messages will be processed. - - restart - Restarts the qrunners, but not the master process. Use this - whenever you upgrade or update Mailman so that the qrunners will - use the newly installed code. - - reopen - This will close all log files, causing them to be re-opened the - next time a message is written to them - -Usage: %prog [options] [ start | stop | restart | reopen ]""")) - parser.add_option('-u', '--run-as-user', - default=True, action='store_false', - help=_("""\ -Normally, this script will refuse to run if the user id and group id are not -set to the `mailman' user and group (as defined when you configured Mailman). -If run as root, this script will change to this user and group before the -check is made. - -This can be inconvenient for testing and debugging purposes, so the -u flag -means that the step that sets and checks the uid/gid is skipped, and the -program is run as the current user and group. This flag is not recommended -for normal production environments. - -Note though, that if you run with -u and are not in the mailman group, you may -have permission problems, such as begin unable to delete a list's archives -through the web. Tough luck!""")) - parser.add_option('-f', '--force', - default=False, action='store_true', - help=_("""\ -If the master watcher finds an existing master lock, it will normally exit -with an error message. With this option,the master will perform an extra -level of checking. If a process matching the host/pid described in the lock -file is running, the master will still exit, requiring you to manually clean -up the lock. But if no matching process is found, the master will remove the -apparently stale lock and make another attempt to claim the master lock.""")) - parser.add_option('-q', '--quiet', - default=False, action='store_true', - help=_("""\ -Don't print status messages. Error messages are still printed to standard -error.""")) - parser.add_option('-C', '--config', - help=_('Alternative configuration file to use')) - options, arguments = parser.parse_args() - if not arguments: - parser.error(_('No command given.')) - if len(arguments) > 1: - commands = COMMASPACE.join(arguments) - parser.error(_('Bad command: $commands')) - parser.options = options - parser.arguments = arguments - return parser - - - -def kill_watcher(sig): - try: - with open(config.PIDFILE) as f: - pid = int(f.read().strip()) - except (IOError, ValueError), e: - # For i18n convenience - print >> sys.stderr, _('PID unreadable in: $config.PIDFILE') - print >> sys.stderr, e - print >> sys.stderr, _('Is qrunner even running?') - return - try: - os.kill(pid, sig) - except OSError, error: - if e.errno <> errno.ESRCH: - raise - print >> sys.stderr, _('No child with pid: $pid') - print >> sys.stderr, e - print >> sys.stderr, _('Stale pid file removed.') - os.unlink(config.PIDFILE) - - - -def check_privileges(): - # If we're running as root (uid == 0), coerce the uid and gid to that - # which Mailman was configured for, and refuse to run if we didn't coerce - # the uid/gid. - gid = grp.getgrnam(config.MAILMAN_GROUP).gr_gid - uid = pwd.getpwnam(config.MAILMAN_USER).pw_uid - myuid = os.getuid() - if myuid == 0: - # Set the process's supplimental groups. - groups = [group.gr_gid for group in grp.getgrall() - if config.MAILMAN_USER in group.gr_mem] - groups.append(gid) - os.setgroups(groups) - os.setgid(gid) - os.setuid(uid) - elif myuid <> uid: - name = config.MAILMAN_USER - parser.error( - _('Run this program as root or as the $name user, or use -u.')) - - - -def main(): - global log, parser - - parser = parseargs() - initialize(parser.options.config) - - log = logging.getLogger('mailman.qrunner') - - if not parser.options.run_as_user: - check_privileges() - else: - if not parser.options.quiet: - print _('Warning! You may encounter permission problems.') - - # Handle the commands - command = parser.arguments[0].lower() - if command == 'stop': - if not parser.options.quiet: - print _("Shutting down Mailman's master qrunner") - kill_watcher(signal.SIGTERM) - elif command == 'restart': - if not parser.options.quiet: - print _("Restarting Mailman's master qrunner") - kill_watcher(signal.SIGUSR1) - elif command == 'reopen': - if not parser.options.quiet: - print _('Re-opening all log files') - kill_watcher(signal.SIGHUP) - elif command == 'start': - # Start the master qrunner watcher process. - # - # Daemon process startup according to Stevens, Advanced Programming in - # the UNIX Environment, Chapter 13. - pid = os.fork() - if pid: - # parent - if not parser.options.quiet: - print _("Starting Mailman's master qrunner.") - return - # child - # - # Create a new session and become the session leader, but since we - # won't be opening any terminal devices, don't do the ultra-paranoid - # suggestion of doing a second fork after the setsid() call. - os.setsid() - # Instead of cd'ing to root, cd to the Mailman runtime directory. - os.chdir(config.VAR_DIR) - # Exec the master watcher. - args = [sys.executable, sys.executable, - os.path.join(config.BIN_DIR, 'master')] - if parser.options.force: - args.append('--force') - if parser.options.config: - args.extend(['-C', parser.options.config]) - log.debug('starting: %s', args) - os.execl(*args) - # We should never get here. - raise RuntimeError('os.execl() failed') - - - -if __name__ == '__main__': - main() diff --git a/mailman/bin/master.py b/mailman/bin/master.py deleted file mode 100644 index d954bc865..000000000 --- a/mailman/bin/master.py +++ /dev/null @@ -1,452 +0,0 @@ -# Copyright (C) 2001-2009 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 . - -"""Master sub-process watcher.""" - -__metaclass__ = type -__all__ = [ - 'Loop', - 'get_lock_data', - ] - - -import os -import sys -import errno -import signal -import socket -import logging - -from datetime import timedelta -from lazr.config import as_boolean -from locknix import lockfile -from munepy import Enum - -from mailman.config import config -from mailman.core.logging import reopen -from mailman.i18n import _ -from mailman.options import Options - - -DOT = '.' -LOCK_LIFETIME = timedelta(days=1, hours=6) -SECONDS_IN_A_DAY = 86400 - - - -class ScriptOptions(Options): - """Options for the master watcher.""" - - usage = _("""\ -Master sub-process watcher. - -Start and watch the configured queue runners and ensure that they stay alive -and kicking. Each are fork and exec'd in turn, with the master waiting on -their process ids. When it detects a child queue runner has exited, it may -restart it. - -The queue runners respond to SIGINT, SIGTERM, SIGUSR1 and SIGHUP. SIGINT, -SIGTERM and SIGUSR1 all cause the qrunners to exit cleanly. The master will -restart qrunners that have exited due to a SIGUSR1 or some kind of other exit -condition (say because of an exception). SIGHUP causes the master and the -qrunners to close their log files, and reopen then upon the next printed -message. - -The master also responds to SIGINT, SIGTERM, SIGUSR1 and SIGHUP, which it -simply passes on to the qrunners. Note that the master will close and reopen -its own log files on receipt of a SIGHUP. The master also leaves its own -process id in the file `data/master-qrunner.pid` but you normally don't need -to use this pid directly. - -Usage: %prog [options]""") - - def add_options(self): - self.parser.add_option( - '-n', '--no-restart', - dest='restartable', default=True, action='store_false', - help=_("""\ -Don't restart the qrunners when they exit because of an error or a SIGUSR1. -Use this only for debugging.""")) - self.parser.add_option( - '-f', '--force', - default=False, action='store_true', - help=_("""\ -If the master watcher finds an existing master lock, it will normally exit -with an error message. With this option,the master will perform an extra -level of checking. If a process matching the host/pid described in the lock -file is running, the master will still exit, requiring you to manually clean -up the lock. But if no matching process is found, the master will remove the -apparently stale lock and make another attempt to claim the master lock.""")) - self.parser.add_option( - '-r', '--runner', - dest='runners', action='append', default=[], - help=_("""\ -Override the default set of queue runners that the master watch will invoke -instead of the default set. Multiple -r options may be given. The values for --r are passed straight through to bin/qrunner.""")) - - def sanity_check(self): - if len(self.arguments) > 0: - self.parser.error(_('Too many arguments')) - - - -def get_lock_data(): - """Get information from the master lock file. - - :return: A 3-tuple of the hostname, integer process id, and file name of - the lock file. - """ - with open(config.LOCK_FILE) as fp: - filename = os.path.split(fp.read().strip())[1] - parts = filename.split('.') - hostname = DOT.join(parts[1:-2]) - pid = int(parts[-2]) - return hostname, int(pid), filename - - -class WatcherState(Enum): - # Another master watcher is running. - conflict = 1 - # No conflicting process exists. - stale_lock = 2 - # Hostname from lock file doesn't match. - host_mismatch = 3 - - -def master_state(): - """Get the state of the master watcher. - - :return: WatcherState describing the state of the lock file. - """ - - # 1 if proc exists on host (but is it qrunner? ;) - # 0 if host matches but no proc - # hostname if hostname doesn't match - hostname, pid, tempfile = get_lock_data() - if hostname <> socket.gethostname(): - return WatcherState.host_mismatch - # Find out if the process exists by calling kill with a signal 0. - try: - os.kill(pid, 0) - return WatcherState.conflict - except OSError, e: - if e.errno == errno.ESRCH: - # No matching process id. - return WatcherState.stale_lock - # Some other error occurred. - raise - - -def acquire_lock_1(force): - """Try to acquire the master queue runner lock. - - :param force: Flag that controls whether to force acquisition of the lock. - :return: The master queue runner lock. - :raises: `TimeOutError` if the lock could not be acquired. - """ - lock = lockfile.Lock(config.LOCK_FILE, LOCK_LIFETIME) - try: - lock.lock(timedelta(seconds=0.1)) - return lock - except lockfile.TimeOutError: - if not force: - raise - # Force removal of lock first. - lock.disown() - hostname, pid, tempfile = get_lock_data() - os.unlink(config.LOCK_FILE) - os.unlink(os.path.join(config.LOCK_DIR, tempfile)) - return acquire_lock_1(force=False) - - -def acquire_lock(force): - """Acquire the master queue runner lock. - - :return: The master queue runner lock or None if the lock couldn't be - acquired. In that case, an error messages is also printed to standard - error. - """ - try: - lock = acquire_lock_1(force) - return lock - except lockfile.TimeOutError: - status = master_state() - if status == WatcherState.conflict: - # Hostname matches and process exists. - message = _("""\ -The master qrunner lock could not be acquired because it appears -as though another master qrunner is already running. -""") - elif status == WatcherState.stale_lock: - # Hostname matches but the process does not exist. - message = _("""\ -The master qrunner lock could not be acquired. It appears as though there is -a stale master qrunner lock. Try re-running mailmanctl with the -s flag. -""") - else: - assert status == WatcherState.host_mismatch, ( - 'Invalid enum value: %s' % status) - # Hostname doesn't even match. - hostname, pid, tempfile = get_lock_data() - message = _("""\ -The master qrunner lock could not be acquired, because it appears as if some -process on some other host may have acquired it. We can't test for stale -locks across host boundaries, so you'll have to clean this up manually. - -Lock file: $config.LOCK_FILE -Lock host: $hostname - -Exiting.""") - config.options.parser.error(message) - - - -class Loop: - """Main control loop class.""" - - def __init__(self, lock=None, restartable=None, config_file=None): - self._lock = lock - self._restartable = restartable - self._config_file = config_file - self._kids = {} - - def install_signal_handlers(self): - """Install various signals handlers for control from mailmanctl.""" - log = logging.getLogger('mailman.qrunner') - # Set up our signal handlers. Also set up a SIGALRM handler to - # refresh the lock once per day. The lock lifetime is 1 day + 6 hours - # so this should be plenty. - def sigalrm_handler(signum, frame): - self._lock.refresh() - signal.alarm(SECONDS_IN_A_DAY) - signal.signal(signal.SIGALRM, sigalrm_handler) - signal.alarm(SECONDS_IN_A_DAY) - # SIGHUP tells the qrunners to close and reopen their log files. - def sighup_handler(signum, frame): - reopen() - for pid in self._kids: - os.kill(pid, signal.SIGHUP) - log.info('Master watcher caught SIGHUP. Re-opening log files.') - signal.signal(signal.SIGHUP, sighup_handler) - # SIGUSR1 is used by 'mailman restart'. - def sigusr1_handler(signum, frame): - for pid in self._kids: - os.kill(pid, signal.SIGUSR1) - log.info('Master watcher caught SIGUSR1. Exiting.') - signal.signal(signal.SIGUSR1, sigusr1_handler) - # SIGTERM is what init will kill this process with when changing run - # levels. It's also the signal 'mailmanctl stop' uses. - def sigterm_handler(signum, frame): - for pid in self._kids: - os.kill(pid, signal.SIGTERM) - log.info('Master watcher caught SIGTERM. Exiting.') - signal.signal(signal.SIGTERM, sigterm_handler) - # SIGINT is what control-C gives. - def sigint_handler(signum, frame): - for pid in self._kids: - os.kill(pid, signal.SIGINT) - log.info('Master watcher caught SIGINT. Restarting.') - signal.signal(signal.SIGINT, sigint_handler) - - def _start_runner(self, spec): - """Start a queue runner. - - All arguments are passed to the qrunner process. - - :param spec: A queue runner spec, in a format acceptable to - bin/qrunner's --runner argument, e.g. name:slice:count - :type spec: string - :return: The process id of the child queue runner. - :rtype: int - """ - pid = os.fork() - if pid: - # Parent. - return pid - # Child. - # - # Craft the command line arguments for the exec() call. - rswitch = '--runner=' + spec - # Wherever mailmanctl lives, so too must live the qrunner script. - exe = os.path.join(config.BIN_DIR, 'qrunner') - # config.PYTHON, which is the absolute path to the Python interpreter, - # must be given as argv[0] due to Python's library search algorithm. - args = [sys.executable, sys.executable, exe, rswitch, '-s'] - if self._config_file is not None: - args.extend(['-C', self._config_file]) - log = logging.getLogger('mailman.qrunner') - log.debug('starting: %s', args) - os.execl(*args) - # We should never get here. - raise RuntimeError('os.execl() failed') - - def start_qrunners(self, qrunner_names=None): - """Start all the configured qrunners. - - :param qrunners: If given, a sequence of queue runner names to start. - If not given, this sequence is taken from the configuration file. - :type qrunners: a sequence of strings - """ - if not qrunner_names: - qrunner_names = [] - for qrunner_config in config.qrunner_configs: - # Strip off the 'qrunner.' prefix. - assert qrunner_config.name.startswith('qrunner.'), ( - 'Unexpected qrunner configuration section name: %s', - qrunner_config.name) - qrunner_names.append(qrunner_config.name[8:]) - # For each qrunner we want to start, find their config section, which - # will tell us the name of the class to instantiate, along with the - # number of hash space slices to manage. - for name in qrunner_names: - section_name = 'qrunner.' + name - # Let AttributeError propagate. - qrunner_config = getattr(config, section_name) - if not as_boolean(qrunner_config.start): - continue - package, class_name = qrunner_config['class'].rsplit(DOT, 1) - __import__(package) - # Let AttributeError propagate. - class_ = getattr(sys.modules[package], class_name) - # Find out how many qrunners to instantiate. This must be a power - # of 2. - count = int(qrunner_config.instances) - assert (count & (count - 1)) == 0, ( - 'Queue runner "%s", not a power of 2: %s', name, count) - for slice_number in range(count): - # qrunner name, slice #, # of slices, restart count - info = (name, slice_number, count, 0) - spec = '%s:%d:%d' % (name, slice_number, count) - pid = self._start_runner(spec) - log = logging.getLogger('mailman.qrunner') - log.debug('[%d] %s', pid, spec) - self._kids[pid] = info - - def loop(self): - """Main loop. - - Wait until all the qrunners have exited, restarting them if necessary - and configured to do so. - """ - log = logging.getLogger('mailman.qrunner') - while True: - try: - pid, status = os.wait() - except OSError, error: - # No children? We're done. - if error.errno == errno.ECHILD: - break - # If the system call got interrupted, just restart it. - elif error.errno == errno.EINTR: - continue - else: - raise - # Find out why the subprocess exited by getting the signal - # received or exit status. - if os.WIFSIGNALED(status): - why = os.WTERMSIG(status) - elif os.WIFEXITED(status): - why = os.WEXITSTATUS(status) - else: - why = None - # We'll restart the subprocess if it exited with a SIGUSR1 or - # because of a failure (i.e. no exit signal), and the no-restart - # command line switch was not given. This lets us better handle - # runaway restarts (e.g. if the subprocess had a syntax error!) - qrname, slice_number, count, restarts = self._kids.pop(pid) - config_name = 'qrunner.' + qrname - restart = False - if why == signal.SIGUSR1 and self._restartable: - restart = True - # Have we hit the maximum number of restarts? - restarts += 1 - max_restarts = int(getattr(config, config_name).max_restarts) - if restarts > max_restarts: - restart = False - # Are we permanently non-restartable? - log.debug("""\ -Master detected subprocess exit -(pid: %d, why: %s, class: %s, slice: %d/%d) %s""", - pid, why, qrname, slice_number + 1, count, - ('[restarting]' if restart else '')) - # See if we've reached the maximum number of allowable restarts - if restarts > max_restarts: - log.info("""\ -qrunner %s reached maximum restart limit of %d, not restarting.""", - qrname, max_restarts) - # Now perhaps restart the process unless it exited with a - # SIGTERM or we aren't restarting. - if restart: - spec = '%s:%d:%d' % (qrname, slice_number, count) - newpid = self._start_runner(spec) - self._kids[newpid] = (qrname, slice_number, count, restarts) - - def cleanup(self): - """Ensure that all children have exited.""" - log = logging.getLogger('mailman.qrunner') - # Send SIGTERMs to all the child processes and wait for them all to - # exit. - for pid in self._kids: - try: - os.kill(pid, signal.SIGTERM) - except OSError, error: - if error.errno == errno.ESRCH: - # The child has already exited. - log.info('ESRCH on pid: %d', pid) - # Wait for all the children to go away. - while self._kids: - try: - pid, status = os.wait() - del self._kids[pid] - except OSError, e: - if e.errno == errno.ECHILD: - break - elif e.errno == errno.EINTR: - continue - raise - - - -def main(): - """Main process.""" - - options = ScriptOptions() - options.initialize() - - # Acquire the master lock, exiting if we can't acquire it. We'll let the - # caller handle any clean up or lock breaking. No with statement here - # because Lock's constructor doesn't support a timeout. - lock = acquire_lock(options.options.force) - try: - with open(config.PIDFILE, 'w') as fp: - print >> fp, os.getpid() - loop = Loop(lock, options.options.restartable, options.options.config) - loop.install_signal_handlers() - try: - loop.start_qrunners(options.options.runners) - loop.loop() - finally: - loop.cleanup() - os.remove(config.PIDFILE) - finally: - lock.unlock() - - - -if __name__ == '__main__': - main() diff --git a/mailman/bin/mmsitepass.py b/mailman/bin/mmsitepass.py deleted file mode 100644 index 132803fc9..000000000 --- a/mailman/bin/mmsitepass.py +++ /dev/null @@ -1,113 +0,0 @@ -# Copyright (C) 1998-2009 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 . - -import sys -import getpass -import optparse - -from mailman import Utils -from mailman import passwords -from mailman.configuration import config -from mailman.i18n import _ -from mailman.initialize import initialize -from mailman.version import MAILMAN_VERSION - - - -def parseargs(): - parser = optparse.OptionParser(version=MAILMAN_VERSION, - usage=_("""\ -%prog [options] [password] - -Set the site or list creator password. - -The site password can be used in most if not all places that the list -administrator's password can be used, which in turn can be used in most places -that a list user's password can be used. The list creator password is a -separate password that can be given to non-site administrators to delegate the -ability to create new mailing lists. - -If password is not given on the command line, it will be prompted for. -""")) - parser.add_option('-c', '--listcreator', - default=False, action='store_true', - help=_("""\ -Set the list creator password instead of the site password. The list -creator is authorized to create and remove lists, but does not have -the total power of the site administrator.""")) - parser.add_option('-p', '--password-scheme', - default='', type='string', - help=_("""\ -Specify the RFC 2307 style hashing scheme for passwords included in the -output. Use -P to get a list of supported schemes, which are -case-insensitive.""")) - parser.add_option('-P', '--list-hash-schemes', - default=False, action='store_true', help=_("""\ -List the supported password hashing schemes and exit. The scheme labels are -case-insensitive.""")) - parser.add_option('-C', '--config', - help=_('Alternative configuration file to use')) - opts, args = parser.parse_args() - if len(args) > 1: - parser.error(_('Unexpected arguments')) - if opts.list_hash_schemes: - for label in passwords.Schemes: - print str(label).upper() - sys.exit(0) - return parser, opts, args - - -def check_password_scheme(parser, password_scheme): - # shoule be checked after config is loaded. - if password_scheme == '': - password_scheme = config.PASSWORD_SCHEME - scheme = passwords.lookup_scheme(password_scheme.lower()) - if not scheme: - parser.error(_('Invalid password scheme')) - return scheme - - - -def main(): - parser, opts, args = parseargs() - initialize(opts.config) - opts.password_scheme = check_password_scheme(parser, opts.password_scheme) - if args: - password = args[0] - else: - # Prompt for the password - if opts.listcreator: - prompt_1 = _('New list creator password: ') - else: - prompt_1 = _('New site administrator password: ') - pw1 = getpass.getpass(prompt_1) - pw2 = getpass.getpass(_('Enter password again to confirm: ')) - if pw1 <> pw2: - print _('Passwords do not match; no changes made.') - sys.exit(1) - password = pw1 - Utils.set_global_password(password, - not opts.listcreator, opts.password_scheme) - if Utils.check_global_password(password, not opts.listcreator): - print _('Password changed.') - else: - print _('Password change failed.') - - - -if __name__ == '__main__': - main() diff --git a/mailman/bin/nightly_gzip.py b/mailman/bin/nightly_gzip.py deleted file mode 100644 index f886e5801..000000000 --- a/mailman/bin/nightly_gzip.py +++ /dev/null @@ -1,117 +0,0 @@ -# Copyright (C) 1998-2009 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 . - -import os -import sys -import optparse - -try: - import gzip -except ImportError: - sys.exit(0) - -from mailman import MailList -from mailman.configuration import config -from mailman.i18n import _ -from mailman.initialize import initialize -from mailman.version import MAILMAN_VERSION - - - -def parseargs(): - parser = optparse.OptionParser(version=MAILMAN_VERSION, - usage=_("""\ -%prog [options] [listname ...] - -Re-generate the Pipermail gzip'd archive flat files.""")) - parser.add_option('-v', '--verbose', - default=False, action='store_true', - help=_("Print each file as it's being gzip'd")) - parser.add_option('-z', '--level', - default=6, type='int', - help=_('Specifies the compression level')) - parser.add_option('-C', '--config', - help=_('Alternative configuration file to use')) - opts, args = parser.parse_args() - if opts.level < 1 or opts.level > 9: - parser.print_help() - print >> sys.stderr, _('Illegal compression level: $opts.level') - sys.exit(1) - return opts, args, parser - - - -def compress(txtfile, opts): - if opts.verbose: - print _("gzip'ing: $txtfile") - infp = outfp = None - try: - infp = open(txtfile) - outfp = gzip.open(txtfile + '.gz', 'wb', opts.level) - outfp.write(infp.read()) - finally: - if outfp: - outfp.close() - if infp: - infp.close() - - - -def main(): - opts, args, parser = parseargs() - initialize(opts.config) - - if config.ARCHIVE_TO_MBOX not in (1, 2) or config.GZIP_ARCHIVE_TXT_FILES: - # We're only going to run the nightly archiver if messages are - # archived to the mbox, and the gzip file is not created on demand - # (i.e. for every individual post). This is the normal mode of - # operation. - return - - # Process all the specified lists - for listname in set(args or config.list_manager.names): - mlist = MailList.MailList(listname, lock=False) - if not mlist.archive: - continue - dir = mlist.archive_dir() - try: - allfiles = os.listdir(dir) - except OSError: - # Has the list received any messages? If not, last_post_time will - # be zero, so it's not really a bogus archive dir. - if mlist.last_post_time > 0: - print _('List $listname has a bogus archive_directory: $dir') - continue - if opts.verbose: - print _('Processing list: $listname') - files = [] - for f in allfiles: - if os.path.splitext(f)[1] <> '.txt': - continue - # stat both the .txt and .txt.gz files and append them only if - # the former is newer than the latter. - txtfile = os.path.join(dir, f) - gzpfile = txtfile + '.gz' - txt_mtime = os.path.getmtime(txtfile) - try: - gzp_mtime = os.path.getmtime(gzpfile) - except OSError: - gzp_mtime = -1 - if txt_mtime > gzp_mtime: - files.append(txtfile) - for f in files: - compress(f, opts) diff --git a/mailman/bin/qrunner.py b/mailman/bin/qrunner.py deleted file mode 100644 index 62e943aad..000000000 --- a/mailman/bin/qrunner.py +++ /dev/null @@ -1,269 +0,0 @@ -# Copyright (C) 2001-2009 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 . - -import sys -import signal -import logging - -from mailman.config import config -from mailman.core.logging import reopen -from mailman.i18n import _ -from mailman.options import Options - - -COMMASPACE = ', ' -log = None - - - -def r_callback(option, opt, value, parser): - dest = getattr(parser.values, option.dest) - parts = value.split(':') - if len(parts) == 1: - runner = parts[0] - rslice = rrange = 1 - elif len(parts) == 3: - runner = parts[0] - try: - rslice = int(parts[1]) - rrange = int(parts[2]) - except ValueError: - parser.print_help() - print >> sys.stderr, _('Bad runner specification: $value') - sys.exit(1) - else: - parser.print_help() - print >> sys.stderr, _('Bad runner specification: $value') - sys.exit(1) - dest.append((runner, rslice, rrange)) - - - -class ScriptOptions(Options): - - usage = _("""\ -Run one or more qrunners, once or repeatedly. - -Each named runner class is run in round-robin fashion. In other words, the -first named runner is run to consume all the files currently in its -directory. When that qrunner is done, the next one is run to consume all the -files in /its/ directory, and so on. The number of total iterations can be -given on the command line. - -Usage: %prog [options] - --r is required unless -l or -h is given, and its argument must be one of the -names displayed by the -l switch. - -Normally, this script should be started from mailmanctl. Running it -separately or with -o is generally useful only for debugging. -""") - - def add_options(self): - self.parser.add_option( - '-r', '--runner', - metavar='runner[:slice:range]', dest='runners', - type='string', default=[], - action='callback', callback=r_callback, - help=_("""\ -Run the named qrunner, which must be one of the strings returned by the -l -option. Optional slice:range if given, is used to assign multiple qrunner -processes to a queue. range is the total number of qrunners for this queue -while slice is the number of this qrunner from [0..range). - -When using the slice:range form, you must ensure that each qrunner for the -queue is given the same range value. If slice:runner is not given, then 1:1 -is used. - -Multiple -r options may be given, in which case each qrunner will run once in -round-robin fashion. The special runner `All' is shorthand for a qrunner for -each listed by the -l option.""")) - self.parser.add_option( - '-o', '--once', - default=False, action='store_true', help=_("""\ -Run each named qrunner exactly once through its main loop. Otherwise, each -qrunner runs indefinitely, until the process receives signal.""")) - self.parser.add_option( - '-l', '--list', - default=False, action='store_true', - help=_('List the available qrunner names and exit.')) - self.parser.add_option( - '-v', '--verbose', - default=0, action='count', help=_("""\ -Display more debugging information to the logs/qrunner log file.""")) - self.parser.add_option( - '-s', '--subproc', - default=False, action='store_true', help=_("""\ -This should only be used when running qrunner as a subprocess of the -mailmanctl startup script. It changes some of the exit-on-error behavior to -work better with that framework.""")) - - def sanity_check(self): - if self.arguments: - self.parser.error(_('Unexpected arguments')) - if not self.options.runners and not self.options.list: - self.parser.error(_('No runner name given.')) - - - -def make_qrunner(name, slice, range, once=False): - # Several conventions for specifying the runner name are supported. It - # could be one of the shortcut names. If the name is a full module path, - # use it explicitly. If the name starts with a dot, it's a class name - # relative to the Mailman.queue package. - qrunner_config = getattr(config, 'qrunner.' + name, None) - if qrunner_config is not None: - # It was a shortcut name. - class_path = qrunner_config['class'] - elif name.startswith('.'): - class_path = 'mailman.queue' + name - else: - class_path = name - module_name, class_name = class_path.rsplit('.', 1) - try: - __import__(module_name) - except ImportError, e: - if config.options.options.subproc: - # Exit with SIGTERM exit code so the master watcher won't try to - # restart us. - print >> sys.stderr, _('Cannot import runner module: $module_name') - print >> sys.stderr, e - sys.exit(signal.SIGTERM) - else: - raise - qrclass = getattr(sys.modules[module_name], class_name) - if once: - # Subclass to hack in the setting of the stop flag in _do_periodic() - class Once(qrclass): - def _do_periodic(self): - self.stop() - qrunner = Once(name, slice) - else: - qrunner = qrclass(name, slice) - return qrunner - - - -def set_signals(loop): - """Set up the signal handlers. - - Signals caught are: SIGTERM, SIGINT, SIGUSR1 and SIGHUP. The latter is - used to re-open the log files. SIGTERM and SIGINT are treated exactly the - same -- they cause qrunner to exit with no restart from the master. - SIGUSR1 also causes qrunner to exit, but the master watcher will restart - it in that case. - - :param loop: A loop queue runner instance. - """ - def sigterm_handler(signum, frame): - # Exit the qrunner cleanly - loop.stop() - loop.status = signal.SIGTERM - log.info('%s qrunner caught SIGTERM. Stopping.', loop.name()) - signal.signal(signal.SIGTERM, sigterm_handler) - def sigint_handler(signum, frame): - # Exit the qrunner cleanly - loop.stop() - loop.status = signal.SIGINT - log.info('%s qrunner caught SIGINT. Stopping.', loop.name()) - signal.signal(signal.SIGINT, sigint_handler) - def sigusr1_handler(signum, frame): - # Exit the qrunner cleanly - loop.stop() - loop.status = signal.SIGUSR1 - log.info('%s qrunner caught SIGUSR1. Stopping.', loop.name()) - signal.signal(signal.SIGUSR1, sigusr1_handler) - # SIGHUP just tells us to rotate our log files. - def sighup_handler(signum, frame): - reopen() - log.info('%s qrunner caught SIGHUP. Reopening logs.', loop.name()) - signal.signal(signal.SIGHUP, sighup_handler) - - - -def main(): - global log - - options = ScriptOptions() - options.initialize() - - if options.options.list: - prefixlen = max(len(shortname) - for shortname in config.qrunner_shortcuts) - for shortname in sorted(config.qrunner_shortcuts): - runnername = config.qrunner_shortcuts[shortname] - shortname = (' ' * (prefixlen - len(shortname))) + shortname - print _('$shortname runs $runnername') - sys.exit(0) - - # Fast track for one infinite runner - if len(options.options.runners) == 1 and not options.options.once: - qrunner = make_qrunner(*options.options.runners[0]) - class Loop: - status = 0 - def __init__(self, qrunner): - self._qrunner = qrunner - def name(self): - return self._qrunner.__class__.__name__ - def stop(self): - self._qrunner.stop() - loop = Loop(qrunner) - set_signals(loop) - # Now start up the main loop - log = logging.getLogger('mailman.qrunner') - log.info('%s qrunner started.', loop.name()) - qrunner.run() - log.info('%s qrunner exiting.', loop.name()) - else: - # Anything else we have to handle a bit more specially - qrunners = [] - for runner, rslice, rrange in options.options.runners: - qrunner = make_qrunner(runner, rslice, rrange, once=True) - qrunners.append(qrunner) - # This class is used to manage the main loop - class Loop: - status = 0 - def __init__(self): - self._isdone = False - def name(self): - return 'Main loop' - def stop(self): - self._isdone = True - def isdone(self): - return self._isdone - loop = Loop() - set_signals(loop) - log.info('Main qrunner loop started.') - while not loop.isdone(): - for qrunner in qrunners: - # In case the SIGTERM came in the middle of this iteration - if loop.isdone(): - break - if options.options.verbose: - log.info('Now doing a %s qrunner iteration', - qrunner.__class__.__bases__[0].__name__) - qrunner.run() - if options.options.once: - break - log.info('Main qrunner loop exiting.') - # All done - sys.exit(loop.status) - - - -if __name__ == '__main__': - main() diff --git a/mailman/bin/remove_list.py b/mailman/bin/remove_list.py deleted file mode 100644 index 05211b200..000000000 --- a/mailman/bin/remove_list.py +++ /dev/null @@ -1,83 +0,0 @@ -# Copyright (C) 1998-2009 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 . - -import sys - -from mailman.app.lifecycle import remove_list -from mailman.config import config -from mailman.i18n import _ -from mailman.options import MultipleMailingListOptions - - - -class ScriptOptions(MultipleMailingListOptions): - usage = _("""\ -%prog [options] - -Remove the components of a mailing list with impunity - beware! - -This removes (almost) all traces of a mailing list. By default, the lists -archives are not removed, which is very handy for retiring old lists. -""") - - def add_options(self): - super(ScriptOptions, self).add_options() - self.parser.add_option( - '-a', '--archives', - default=False, action='store_true', - help=_("""\ -Remove the list's archives too, or if the list has already been deleted, -remove any residual archives.""")) - self.parser.add_option( - '-q', '--quiet', - default=False, action='store_true', - help=_('Suppress status messages')) - - def sanity_check(self): - if len(self.options.listnames) == 0: - self.parser.error(_('Nothing to do')) - if len(self.arguments) > 0: - self.parser.error(_('Unexpected arguments')) - - - -def main(): - options = ScriptOptions() - options.initialize() - - for fqdn_listname in options.options.listnames: - if not options.options.quiet: - print _('Removing list: $fqdn_listname') - mlist = config.db.list_manager.get(fqdn_listname) - if mlist is None: - if options.options.archives: - print _("""\ -No such list: ${fqdn_listname}. Removing its residual archives.""") - else: - print >> sys.stderr, _( - 'No such list (or list already deleted): $fqdn_listname') - - if not options.options.archives: - print _('Not removing archives. Reinvoke with -a to remove them.') - - remove_list(fqdn_listname, mlist, options.options.archives) - config.db.commit() - - - -if __name__ == '__main__': - main() diff --git a/mailman/bin/senddigests.py b/mailman/bin/senddigests.py deleted file mode 100644 index fb057d6b9..000000000 --- a/mailman/bin/senddigests.py +++ /dev/null @@ -1,83 +0,0 @@ -# Copyright (C) 1998-2009 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 . - -import os -import sys -import optparse - -from mailman import MailList -from mailman.i18n import _ -from mailman.initialize import initialize -from mailman.version import MAILMAN_VERSION - -# Work around known problems with some RedHat cron daemons -import signal -signal.signal(signal.SIGCHLD, signal.SIG_DFL) - - - -def parseargs(): - parser = optparse.OptionParser(version=MAILMAN_VERSION, - usage=_("""\ -%prog [options] - -Dispatch digests for lists w/pending messages and digest_send_periodic -set.""")) - parser.add_option('-l', '--listname', - type='string', default=[], action='append', - dest='listnames', help=_("""\ -Send the digest for the given list only, otherwise the digests for all -lists are sent out. Multiple -l options may be given.""")) - parser.add_option('-C', '--config', - help=_('Alternative configuration file to use')) - opts, args = parser.parse_args() - if args: - parser.print_help() - print >> sys.stderr, _('Unexpected arguments') - sys.exit(1) - return opts, args, parser - - - -def main(): - opts, args, parser = parseargs() - initialize(opts.config) - - for listname in set(opts.listnames or config.list_manager.names): - mlist = MailList.MailList(listname, lock=False) - if mlist.digest_send_periodic: - mlist.Lock() - try: - try: - mlist.send_digest_now() - mlist.Save() - # We are unable to predict what exception may occur in digest - # processing and we don't want to lose the other digests, so - # we catch everything. - except Exception, errmsg: - print >> sys.stderr, \ - 'List: %s: problem processing %s:\n%s' % \ - (listname, - os.path.join(mlist.data_path, 'digest.mbox'), - errmsg) - finally: - mlist.Unlock() - - - -if __name__ == '__main__': - main() diff --git a/mailman/bin/set_members.py b/mailman/bin/set_members.py deleted file mode 100644 index cdd11c56f..000000000 --- a/mailman/bin/set_members.py +++ /dev/null @@ -1,189 +0,0 @@ -# Copyright (C) 2007-2009 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 . - -import csv -import optparse - -from mailman import Message -from mailman import Utils -from mailman import i18n -from mailman import passwords -from mailman.app.membership import add_member -from mailman.app.notifications import ( - send_admin_subscription_notice, send_welcome_message) -from mailman.configuration import config -from mailman.initialize import initialize -from mailman.interfaces import DeliveryMode -from mailman.version import MAILMAN_VERSION - - -_ = i18n._ - -DELIVERY_MODES = { - 'regular': DeliveryMode.regular, - 'plain': DeliveryMode.plaintext_digests, - 'mime': DeliveryMode.mime_digests, - } - - - -def parseargs(): - parser = optparse.OptionParser(version=MAILMAN_VERSION, - usage=_("""\ -%prog [options] csv-file - -Set the membership of a mailing list to that described in a CSV file. Each -row of the CSV file has the following format. Only the address column is -required. - - - email address - - full name (default: the empty string) - - delivery mode (default: regular delivery) [1] - -[1] The delivery mode is a case insensitive string of the following values: - - regular - regular, i.e. immediate delivery - mime - MIME digest delivery - plain - plain text (RFC 1153) digest delivery - -Any address not included in the CSV file is removed from the list membership. -""")) - parser.add_option('-l', '--listname', - type='string', help=_("""\ -Mailng list to set the membership for.""")) - parser.add_option('-w', '--welcome-msg', - type='string', metavar='', help=_("""\ -Set whether or not to send the list members a welcome message, overriding -whatever the list's 'send_welcome_msg' setting is.""")) - parser.add_option('-a', '--admin-notify', - type='string', metavar='', help=_("""\ -Set whether or not to send the list administrators a notification on the -success/failure of these subscriptions, overriding whatever the list's -'admin_notify_mchanges' setting is.""")) - parser.add_option('-v', '--verbose', action='store_true', - help=_('Increase verbosity')) - parser.add_option('-C', '--config', - help=_('Alternative configuration file to use')) - opts, args = parser.parse_args() - if opts.welcome_msg is not None: - ch = opts.welcome_msg[0].lower() - if ch == 'y': - opts.welcome_msg = True - elif ch == 'n': - opts.welcome_msg = False - else: - parser.error(_('Illegal value for -w: $opts.welcome_msg')) - if opts.admin_notify is not None: - ch = opts.admin_notify[0].lower() - if ch == 'y': - opts.admin_notify = True - elif ch == 'n': - opts.admin_notify = False - else: - parser.error(_('Illegal value for -a: $opts.admin_notify')) - return parser, opts, args - - - -def parse_file(filename): - members = {} - with open(filename) as fp: - for row in csv.reader(fp): - if len(row) == 0: - continue - elif len(row) == 1: - address = row[0] - real_name = None - delivery_mode = DeliveryMode.regular - elif len(row) == 2: - address, real_name = row - delivery_mode = DeliveryMode.regular - else: - # Ignore extra columns - address, real_name = row[0:2] - delivery_mode = DELIVERY_MODES.get(row[2].lower()) - if delivery_mode is None: - delivery_mode = DeliveryMode.regular - members[address] = real_name, delivery_mode - return members - - - -def main(): - parser, opts, args = parseargs() - initialize(opts.config) - - mlist = config.db.list_manager.get(opts.listname) - if mlist is None: - parser.error(_('No such list: $opts.listname')) - - # Set up defaults. - if opts.welcome_msg is None: - send_welcome_msg = mlist.send_welcome_msg - else: - send_welcome_msg = opts.welcome_msg - if opts.admin_notify is None: - admin_notify = mlist.admin_notify_mchanges - else: - admin_notify = opts.admin_notify - - # Parse the csv files. - member_data = {} - for filename in args: - member_data.update(parse_file(filename)) - - future_members = set(member_data) - current_members = set(obj.address for obj in mlist.members.addresses) - add_members = future_members - current_members - delete_members = current_members - future_members - change_members = current_members & future_members - - with i18n.using_language(mlist.preferred_language): - # Start by removing all the delete members. - for address in delete_members: - print _('deleting address: $address') - member = mlist.members.get_member(address) - member.unsubscribe() - # For all members that are in both lists, update their full name and - # delivery mode. - for address in change_members: - print _('updating address: $address') - real_name, delivery_mode = member_data[address] - member = mlist.members.get_member(address) - member.preferences.delivery_mode = delivery_mode - user = config.db.user_manager.get_user(address) - user.real_name = real_name - for address in add_members: - print _('adding address: $address') - real_name, delivery_mode = member_data[address] - password = passwords.make_secret( - Utils.MakeRandomPassword(), - passwords.lookup_scheme(config.PASSWORD_SCHEME)) - add_member(mlist, address, real_name, password, delivery_mode, - mlist.preferred_language, send_welcome_msg, - admin_notify) - if send_welcome_msg: - send_welcome_message(mlist, address, language, delivery_mode) - if admin_notify: - send_admin_subscription_notice(mlist, address, real_name) - - config.db.flush() - - - -if __name__ == '__main__': - main() diff --git a/mailman/bin/show_config.py b/mailman/bin/show_config.py deleted file mode 100644 index 8d26c5c97..000000000 --- a/mailman/bin/show_config.py +++ /dev/null @@ -1,97 +0,0 @@ -# Copyright (C) 2006-2009 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 . - -import re -import sys -import pprint -import optparse - -from mailman.configuration import config -from mailman.i18n import _ -from mailman.version import MAILMAN_VERSION - - -# List of names never to show even if --verbose -NEVER_SHOW = ['__builtins__', '__doc__'] - - - -def parseargs(): - parser = optparse.OptionParser(version=MAILMAN_VERSION, - usage=_("""\ -%%prog [options] [pattern ...] - -Show the values of various Defaults.py/mailman.cfg variables. -If one or more patterns are given, show only those variables -whose names match a pattern""")) - parser.add_option('-v', '--verbose', - default=False, action='store_true', - help=_( -"Show all configuration names, not just 'settings'.")) - parser.add_option('-i', '--ignorecase', - default=False, action='store_true', - help=_("Match patterns case-insensitively.")) - parser.add_option('-C', '--config', - help=_('Alternative configuration file to use')) - opts, args = parser.parse_args() - return parser, opts, args - - - -def main(): - parser, opts, args = parseargs() - - patterns = [] - if opts.ignorecase: - flag = re.IGNORECASE - else: - flag = 0 - for pattern in args: - patterns.append(re.compile(pattern, flag)) - - pp = pprint.PrettyPrinter(indent=4) - config.load(opts.config) - names = config.__dict__.keys() - names.sort() - for name in names: - if name in NEVER_SHOW: - continue - if not opts.verbose: - if name.startswith('_') or re.search('[a-z]', name): - continue - if patterns: - hit = False - for pattern in patterns: - if pattern.search(name): - hit = True - break - if not hit: - continue - value = config.__dict__[name] - if isinstance(value, str): - if re.search('\n', value): - print '%s = """%s"""' %(name, value) - else: - print "%s = '%s'" % (name, value) - else: - print '%s = ' % name, - pp.pprint(value) - - - -if __name__ == '__main__': - main() diff --git a/mailman/bin/show_qfiles.py b/mailman/bin/show_qfiles.py deleted file mode 100644 index e4b64e0cd..000000000 --- a/mailman/bin/show_qfiles.py +++ /dev/null @@ -1,91 +0,0 @@ -# Copyright (C) 2006-2009 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 . - -import os -import sys - -from cPickle import load - -from mailman.config import config -from mailman.i18n import _ -from mailman.options import Options - - - -class ScriptOptions(Options): - usage = _(""" -%%prog [options] qfiles ... - -Show the contents of one or more Mailman queue files.""") - - def add_options(self): - super(ScriptOptions, self).add_options() - self.parser.add_option( - '-q', '--quiet', - default=False, action='store_true', - help=_("Don't print 'helpful' message delimiters.")) - self.parser.add_option( - '-s', '--summary', - default=False, action='store_true', - help=_('Show a summary of queue files.')) - - - -def main(): - options = ScriptOptions() - options.initialize() - - if options.options.summary: - queue_totals = {} - files_by_queue = {} - for switchboard in config.switchboards.values(): - total = 0 - file_mappings = {} - for filename in os.listdir(switchboard.queue_directory): - base, ext = os.path.splitext(filename) - file_mappings[ext] = file_mappings.get(ext, 0) + 1 - total += 1 - files_by_queue[switchboard.queue_directory] = file_mappings - queue_totals[switchboard.queue_directory] = total - # Sort by queue name. - for queue_directory in sorted(files_by_queue): - total = queue_totals[queue_directory] - print queue_directory - print _('\tfile count: $total') - file_mappings = files_by_queue[queue_directory] - for ext in sorted(file_mappings): - print '\t{0}: {1}'.format(ext, file_mappings[ext]) - return - # No summary. - for filename in options.arguments: - if not options.options.quiet: - print '====================>', filename - with open(filename) as fp: - if filename.endswith('.pck'): - msg = load(fp) - data = load(fp) - if data.get('_parsemsg'): - sys.stdout.write(msg) - else: - sys.stdout.write(msg.as_string()) - else: - sys.stdout.write(fp.read()) - - - -if __name__ == '__main__': - main() diff --git a/mailman/bin/unshunt.py b/mailman/bin/unshunt.py deleted file mode 100644 index fc889377c..000000000 --- a/mailman/bin/unshunt.py +++ /dev/null @@ -1,51 +0,0 @@ -# Copyright (C) 2002-2009 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 . - -__metaclass__ = type -__all__ = [ - 'main', - ] - - -import sys - -from mailman.config import config -from mailman.i18n import _ -from mailman.options import Options - - - -def main(): - options = Options() - options.initialize() - - switchboard = config.switchboards['shunt'] - switchboard.recover_backup_files() - - for filebase in switchboard.files: - try: - msg, msgdata = switchboard.dequeue(filebase) - whichq = msgdata.get('whichq', 'in') - config.switchboards[whichq].enqueue(msg, msgdata) - except Exception, e: - # If there are any unshunting errors, log them and continue trying - # other shunted messages. - print >> sys.stderr, _( - 'Cannot unshunt message $filebase, skipping:\n$e') - else: - # Unlink the .bak file left by dequeue() - switchboard.finish(filebase) diff --git a/mailman/bin/update.py b/mailman/bin/update.py deleted file mode 100644 index 34ea6cda3..000000000 --- a/mailman/bin/update.py +++ /dev/null @@ -1,660 +0,0 @@ -# Copyright (C) 1998-2009 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 . - -import os -import md5 -import sys -import time -import email -import errno -import shutil -import cPickle -import marshal -import optparse - -from locknix.lockfile import TimeOutError - -from mailman import MailList -from mailman import Message -from mailman import Pending -from mailman import Utils -from mailman import version -from mailman.MemberAdaptor import BYBOUNCE, ENABLED -from mailman.OldStyleMemberships import OldStyleMemberships -from mailman.Queue.Switchboard import Switchboard -from mailman.configuration import config -from mailman.i18n import _ -from mailman.initialize import initialize -from mailman.utilities.filesystem import makedirs - - -FRESH = 0 -NOTFRESH = -1 - - - -def parseargs(): - parser = optparse.OptionParser(version=version.MAILMAN_VERSION, - usage=_("""\ -Perform all necessary upgrades. - -%prog [options]""")) - parser.add_option('-f', '--force', - default=False, action='store_true', help=_("""\ -Force running the upgrade procedures. Normally, if the version number of the -installed Mailman matches the current version number (or a 'downgrade' is -detected), nothing will be done.""")) - parser.add_option('-C', '--config', - help=_('Alternative configuration file to use')) - opts, args = parser.parse_args() - if args: - parser.print_help() - print >> sys.stderr, _('Unexpected arguments') - sys.exit(1) - return parser, opts, args - - - -def calcversions(): - # Returns a tuple of (lastversion, thisversion). If the last version - # could not be determined, lastversion will be FRESH or NOTFRESH, - # depending on whether this installation appears to be fresh or not. The - # determining factor is whether there are files in the $var_prefix/logs - # subdir or not. The version numbers are HEX_VERSIONs. - # - # See if we stored the last updated version - lastversion = None - thisversion = version.HEX_VERSION - try: - fp = open(os.path.join(config.DATA_DIR, 'last_mailman_version')) - data = fp.read() - fp.close() - lastversion = int(data, 16) - except (IOError, ValueError): - pass - # - # try to figure out if this is a fresh install - if lastversion is None: - lastversion = FRESH - try: - if os.listdir(config.LOG_DIR): - lastversion = NOTFRESH - except OSError: - pass - return (lastversion, thisversion) - - - -def makeabs(relpath): - return os.path.join(config.PREFIX, relpath) - - -def make_varabs(relpath): - return os.path.join(config.VAR_PREFIX, relpath) - - - -def move_language_templates(mlist): - listname = mlist.internal_name() - print _('Fixing language templates: $listname') - # Mailman 2.1 has a new cascading search for its templates, defined and - # described in Utils.py:maketext(). Putting templates in the top level - # templates/ subdir or the lists/ subdir is deprecated and no - # longer searched.. - # - # What this means is that most templates can live in the global templates/ - # subdirectory, and only needs to be copied into the list-, vhost-, or - # site-specific language directories when needed. - # - # Also, by default all standard (i.e. English) templates must now live in - # the templates/en directory. This update cleans up all the templates, - # deleting more-specific duplicates (as calculated by md5 checksums) in - # favor of more-global locations. - # - # First, get rid of any lists/ template or lists//en template - # that is identical to the global templates/* default. - for gtemplate in os.listdir(os.path.join(config.TEMPLATE_DIR, 'en')): - # BAW: get rid of old templates, e.g. admlogin.txt and - # handle_opts.html - try: - fp = open(os.path.join(config.TEMPLATE_DIR, gtemplate)) - except IOError, e: - if e.errno <> errno.ENOENT: - raise - # No global template - continue - gcksum = md5.new(fp.read()).digest() - fp.close() - # Match against the lists//* template - try: - fp = open(os.path.join(mlist.fullpath(), gtemplate)) - except IOError, e: - if e.errno <> errno.ENOENT: - raise - else: - tcksum = md5.new(fp.read()).digest() - fp.close() - if gcksum == tcksum: - os.unlink(os.path.join(mlist.fullpath(), gtemplate)) - # Match against the lists//*.prev template - try: - fp = open(os.path.join(mlist.fullpath(), gtemplate + '.prev')) - except IOError, e: - if e.errno <> errno.ENOENT: - raise - else: - tcksum = md5.new(fp.read()).digest() - fp.close() - if gcksum == tcksum: - os.unlink(os.path.join(mlist.fullpath(), gtemplate + '.prev')) - # Match against the lists//en/* templates - try: - fp = open(os.path.join(mlist.fullpath(), 'en', gtemplate)) - except IOError, e: - if e.errno <> errno.ENOENT: - raise - else: - tcksum = md5.new(fp.read()).digest() - fp.close() - if gcksum == tcksum: - os.unlink(os.path.join(mlist.fullpath(), 'en', gtemplate)) - # Match against the templates/* template - try: - fp = open(os.path.join(config.TEMPLATE_DIR, gtemplate)) - except IOError, e: - if e.errno <> errno.ENOENT: - raise - else: - tcksum = md5.new(fp.read()).digest() - fp.close() - if gcksum == tcksum: - os.unlink(os.path.join(config.TEMPLATE_DIR, gtemplate)) - # Match against the templates/*.prev template - try: - fp = open(os.path.join(config.TEMPLATE_DIR, gtemplate + '.prev')) - except IOError, e: - if e.errno <> errno.ENOENT: - raise - else: - tcksum = md5.new(fp.read()).digest() - fp.close() - if gcksum == tcksum: - os.unlink(os.path.join(config.TEMPLATE_DIR, - gtemplate + '.prev')) - - - -def situate_list(listname): - # This turns the directory called 'listname' into a directory called - # 'listname@domain'. Start by finding out what the domain should be. - # A list's domain is its email host. - mlist = MailList.MailList(listname, lock=False, check_version=False) - fullname = mlist.fqdn_listname - oldpath = os.path.join(config.VAR_PREFIX, 'lists', listname) - newpath = os.path.join(config.VAR_PREFIX, 'lists', fullname) - if os.path.exists(newpath): - print >> sys.stderr, _('WARNING: could not situate list: $listname') - else: - os.rename(oldpath, newpath) - print _('situated list $listname to $fullname') - return fullname - - - -def dolist(listname): - mlist = MailList.MailList(listname, lock=False) - try: - mlist.Lock(0.5) - except TimeOutError: - print >> sys.stderr, _( - 'WARNING: could not acquire lock for list: $listname') - return 1 - # Sanity check the invariant that every BYBOUNCE disabled member must have - # bounce information. Some earlier betas broke this. BAW: we're - # submerging below the MemberAdaptor interface, so skip this if we're not - # using OldStyleMemberships. - if isinstance(mlist._memberadaptor, OldStyleMemberships): - noinfo = {} - for addr, (reason, when) in mlist.delivery_status.items(): - if reason == BYBOUNCE and not mlist.bounce_info.has_key(addr): - noinfo[addr] = reason, when - # What to do about these folks with a BYBOUNCE delivery status and no - # bounce info? This number should be very small, and I think it's - # fine to simple re-enable them and let the bounce machinery - # re-disable them if necessary. - n = len(noinfo) - if n > 0: - print _( - 'Resetting $n BYBOUNCEs disabled addrs with no bounce info') - for addr in noinfo.keys(): - mlist.setDeliveryStatus(addr, ENABLED) - - mbox_dir = make_varabs('archives/private/%s.mbox' % (listname)) - mbox_file = make_varabs('archives/private/%s.mbox/%s' % (listname, - listname)) - o_pub_mbox_file = make_varabs('archives/public/%s' % (listname)) - o_pri_mbox_file = make_varabs('archives/private/%s' % (listname)) - html_dir = o_pri_mbox_file - o_html_dir = makeabs('public_html/archives/%s' % (listname)) - # Make the mbox directory if it's not there. - if not os.path.exists(mbox_dir): - makedirs(mbox_dir) - else: - # This shouldn't happen, but hey, just in case - if not os.path.isdir(mbox_dir): - print _("""\ -For some reason, $mbox_dir exists as a file. This won't work with b6, so I'm -renaming it to ${mbox_dir}.tmp and proceeding.""") - os.rename(mbox_dir, "%s.tmp" % (mbox_dir)) - makedirs(mbox_dir) - # Move any existing mboxes around, but watch out for both a public and a - # private one existing - if os.path.isfile(o_pri_mbox_file) and os.path.isfile(o_pub_mbox_file): - if mlist.archive_private: - print _("""\ - -$listname has both public and private mbox archives. Since this list -currently uses private archiving, I'm installing the private mbox archive -- -$o_pri_mbox_file -- as the active archive, and renaming - $o_pub_mbox_file -to - ${o_pub_mbox_file}.preb6 - -You can integrate that into the archives if you want by using the 'arch' -script. -""") % (mlist._internal_name, o_pri_mbox_file, o_pub_mbox_file, - o_pub_mbox_file) - os.rename(o_pub_mbox_file, "%s.preb6" % (o_pub_mbox_file)) - else: - print _("""\ -$mlist._internal_name has both public and private mbox archives. Since this -list currently uses public archiving, I'm installing the public mbox file -archive file ($o_pub_mbox_file) as the active one, and renaming -$o_pri_mbox_file to ${o_pri_mbox_file}.preb6 - -You can integrate that into the archives if you want by using the 'arch' -script. -""") - os.rename(o_pri_mbox_file, "%s.preb6" % (o_pri_mbox_file)) - # Move private archive mbox there if it's around - # and take into account all sorts of absurdities - print _('- updating old private mbox file') - if os.path.exists(o_pri_mbox_file): - if os.path.isfile(o_pri_mbox_file): - os.rename(o_pri_mbox_file, mbox_file) - elif not os.path.isdir(o_pri_mbox_file): - newname = "%s.mm_install-dunno_what_this_was_but_its_in_the_way" \ - % o_pri_mbox_file - os.rename(o_pri_mbox_file, newname) - print _("""\ - unknown file in the way, moving - $o_pri_mbox_file - to - $newname""") - else: - # directory - print _("""\ - looks like you have a really recent development installation... - you're either one brave soul, or you already ran me""") - # Move public archive mbox there if it's around - # and take into account all sorts of absurdities. - print _('- updating old public mbox file') - if os.path.exists(o_pub_mbox_file): - if os.path.isfile(o_pub_mbox_file): - os.rename(o_pub_mbox_file, mbox_file) - elif not os.path.isdir(o_pub_mbox_file): - newname = "%s.mm_install-dunno_what_this_was_but_its_in_the_way" \ - % o_pub_mbox_file - os.rename(o_pub_mbox_file, newname) - print _("""\ - unknown file in the way, moving - $o_pub_mbox_file - to - $newname""") - else: # directory - print _("""\ - looks like you have a really recent development installation... - you're either one brave soul, or you already ran me""") - # Move the html archives there - if os.path.isdir(o_html_dir): - os.rename(o_html_dir, html_dir) - # chmod the html archives - os.chmod(html_dir, 02775) - # BAW: Is this still necessary?! - mlist.Save() - # Check to see if pre-b4 list-specific templates are around - # and move them to the new place if there's not already - # a new one there - tmpl_dir = os.path.join(config.PREFIX, "templates") - list_dir = os.path.join(config.PREFIX, "lists") - b4_tmpl_dir = os.path.join(tmpl_dir, mlist._internal_name) - new_tmpl_dir = os.path.join(list_dir, mlist._internal_name) - if os.path.exists(b4_tmpl_dir): - print _("""\ -- This list looks like it might have <= b4 list templates around""") - for f in os.listdir(b4_tmpl_dir): - o_tmpl = os.path.join(b4_tmpl_dir, f) - n_tmpl = os.path.join(new_tmpl_dir, f) - if os.path.exists(o_tmpl): - if not os.path.exists(n_tmpl): - os.rename(o_tmpl, n_tmpl) - print _('- moved $o_tmpl to $n_tmpl') - else: - print _("""\ -- both $o_tmpl and $n_tmpl exist, leaving untouched""") - else: - print _("""\ -- $o_tmpl doesn't exist, leaving untouched""") - # Move all the templates to the en language subdirectory as required for - # Mailman 2.1 - move_language_templates(mlist) - # Avoid eating filehandles with the list lockfiles - mlist.Unlock() - return 0 - - - -def archive_path_fixer(unused_arg, dir, files): - # Passed to os.path.walk to fix the perms on old html archives. - for f in files: - abs = os.path.join(dir, f) - if os.path.isdir(abs): - if f == "database": - os.chmod(abs, 02770) - else: - os.chmod(abs, 02775) - elif os.path.isfile(abs): - os.chmod(abs, 0664) - - -def remove_old_sources(module): - # Also removes old directories. - src = '%s/%s' % (config.PREFIX, module) - pyc = src + "c" - if os.path.isdir(src): - print _('removing directory $src and everything underneath') - shutil.rmtree(src) - elif os.path.exists(src): - print _('removing $src') - try: - os.unlink(src) - except os.error, rest: - print _("Warning: couldn't remove $src -- $rest") - if module.endswith('.py') and os.path.exists(pyc): - try: - os.unlink(pyc) - except OSError, rest: - print _("couldn't remove old file $pyc -- $rest") - - - -def update_qfiles(): - print _('updating old qfiles') - prefix = `time.time()` + '+' - # Be sure the qfiles/in directory exists (we don't really need the - # switchboard object, but it's convenient for creating the directory). - sb = Switchboard(config.INQUEUE_DIR) - for filename in os.listdir(config.QUEUE_DIR): - # Updating means just moving the .db and .msg files to qfiles/in where - # it should be dequeued, converted, and processed normally. - if os.path.splitext(filename) == '.msg': - oldmsgfile = os.path.join(config.QUEUE_DIR, filename) - newmsgfile = os.path.join(config.INQUEUE_DIR, prefix + filename) - os.rename(oldmsgfile, newmsgfile) - elif os.path.splitext(filename) == '.db': - olddbfile = os.path.join(config.QUEUE_DIR, filename) - newdbfile = os.path.join(config.INQUEUE_DIR, prefix + filename) - os.rename(olddbfile, newdbfile) - # Now update for the Mailman 2.1.5 qfile format. For every filebase in - # the qfiles/* directories that has both a .pck and a .db file, pull the - # data out and re-queue them. - for dirname in os.listdir(config.QUEUE_DIR): - dirpath = os.path.join(config.QUEUE_DIR, dirname) - if dirpath == config.BADQUEUE_DIR: - # The files in qfiles/bad can't possibly be pickles - continue - sb = Switchboard(dirpath) - try: - for filename in os.listdir(dirpath): - filepath = os.path.join(dirpath, filename) - filebase, ext = os.path.splitext(filepath) - # Handle the .db metadata files as part of the handling of the - # .pck or .msg message files. - if ext not in ('.pck', '.msg'): - continue - msg, data = dequeue(filebase) - if msg is not None and data is not None: - sb.enqueue(msg, data) - except EnvironmentError, e: - if e.errno <> errno.ENOTDIR: - raise - print _('Warning! Not a directory: $dirpath') - - - -# Implementations taken from the pre-2.1.5 Switchboard -def ext_read(filename): - fp = open(filename) - d = marshal.load(fp) - # Update from version 2 files - if d.get('version', 0) == 2: - del d['filebase'] - # Do the reverse conversion (repr -> float) - for attr in ['received_time']: - try: - sval = d[attr] - except KeyError: - pass - else: - # Do a safe eval by setting up a restricted execution - # environment. This may not be strictly necessary since we - # know they are floats, but it can't hurt. - d[attr] = eval(sval, {'__builtins__': {}}) - fp.close() - return d - - -def dequeue(filebase): - # Calculate the .db and .msg filenames from the given filebase. - msgfile = os.path.join(filebase + '.msg') - pckfile = os.path.join(filebase + '.pck') - dbfile = os.path.join(filebase + '.db') - # Now we are going to read the message and metadata for the given - # filebase. We want to read things in this order: first, the metadata - # file to find out whether the message is stored as a pickle or as - # plain text. Second, the actual message file. However, we want to - # first unlink the message file and then the .db file, because the - # qrunner only cues off of the .db file - msg = None - try: - data = ext_read(dbfile) - os.unlink(dbfile) - except EnvironmentError, e: - if e.errno <> errno.ENOENT: - raise - data = {} - # Between 2.1b4 and 2.1b5, the `rejection-notice' key in the metadata - # was renamed to `rejection_notice', since dashes in the keys are not - # supported in METAFMT_ASCII. - if data.has_key('rejection-notice'): - data['rejection_notice'] = data['rejection-notice'] - del data['rejection-notice'] - msgfp = None - try: - try: - msgfp = open(pckfile) - msg = cPickle.load(msgfp) - os.unlink(pckfile) - except EnvironmentError, e: - if e.errno <> errno.ENOENT: raise - msgfp = None - try: - msgfp = open(msgfile) - msg = email.message_from_file(msgfp, Message.Message) - os.unlink(msgfile) - except EnvironmentError, e: - if e.errno <> errno.ENOENT: raise - except (email.Errors.MessageParseError, ValueError), e: - # This message was unparsable, most likely because its - # MIME encapsulation was broken. For now, there's not - # much we can do about it. - print _('message is unparsable: $filebase') - msgfp.close() - msgfp = None - if config.QRUNNER_SAVE_BAD_MESSAGES: - # Cheapo way to ensure the directory exists w/ the - # proper permissions. - sb = Switchboard(config.BADQUEUE_DIR) - os.rename(msgfile, os.path.join( - config.BADQUEUE_DIR, filebase + '.txt')) - else: - os.unlink(msgfile) - msg = data = None - except EOFError: - # For some reason the pckfile was empty. Just delete it. - print _('Warning! Deleting empty .pck file: $pckfile') - os.unlink(pckfile) - finally: - if msgfp: - msgfp.close() - return msg, data - - - -def main(): - parser, opts, args = parseargs() - initialize(opts.config) - - # calculate the versions - lastversion, thisversion = calcversions() - hexlversion = hex(lastversion) - hextversion = hex(thisversion) - if lastversion == thisversion and not opts.force: - # nothing to do - print _('No updates are necessary.') - sys.exit(0) - if lastversion > thisversion and not opts.force: - print _("""\ -Downgrade detected, from version $hexlversion to version $hextversion -This is probably not safe. -Exiting.""") - sys.exit(1) - print _('Upgrading from version $hexlversion to $hextversion') - errors = 0 - # get rid of old stuff - print _('getting rid of old source files') - for mod in ('mailman/Archiver.py', 'mailman/HyperArch.py', - 'mailman/HyperDatabase.py', 'mailman/pipermail.py', - 'mailman/smtplib.py', 'mailman/Cookie.py', - 'bin/update_to_10b6', 'scripts/mailcmd', - 'scripts/mailowner', 'mail/wrapper', 'mailman/pythonlib', - 'cgi-bin/archives', 'mailman/MailCommandHandler'): - remove_old_sources(mod) - if not config.list_manager.names: - print _('no lists == nothing to do, exiting') - return - # For people with web archiving, make sure the directories - # in the archiving are set with proper perms for b6. - if os.path.isdir("%s/public_html/archives" % config.PREFIX): - print _("""\ -fixing all the perms on your old html archives to work with b6 -If your archives are big, this could take a minute or two...""") - os.path.walk("%s/public_html/archives" % config.PREFIX, - archive_path_fixer, "") - print _('done') - for listname in config.list_manager.names: - # With 2.2.0a0, all list names grew an @domain suffix. If you find a - # list without that, move it now. - if not '@' in listname: - listname = situate_list(listname) - print _('Updating mailing list: $listname') - errors += dolist(listname) - print - print _('Updating Usenet watermarks') - wmfile = os.path.join(config.DATA_DIR, 'gate_watermarks') - try: - fp = open(wmfile) - except IOError: - print _('- nothing to update here') - else: - d = marshal.load(fp) - fp.close() - for listname in d.keys(): - if listname not in listnames: - # this list no longer exists - continue - mlist = MailList.MailList(listname, lock=0) - try: - mlist.Lock(0.5) - except TimeOutError: - print >> sys.stderr, _( - 'WARNING: could not acquire lock for list: $listname') - errors = errors + 1 - else: - # Pre 1.0b7 stored 0 in the gate_watermarks file to indicate - # that no gating had been done yet. Without coercing this to - # None, the list could now suddenly get flooded. - mlist.usenet_watermark = d[listname] or None - mlist.Save() - mlist.Unlock() - os.unlink(wmfile) - print _('- usenet watermarks updated and gate_watermarks removed') - # In Mailman 2.1, the qfiles directory has a different structure and a - # different content. Also, in Mailman 2.1.5 we collapsed the message - # files from separate .msg (pickled Message objects) and .db (marshalled - # dictionaries) to a shared .pck file containing two pickles. - update_qfiles() - # This warning was necessary for the upgrade from 1.0b9 to 1.0b10. - # There's no good way of figuring this out for releases prior to 2.0beta2 - # :( - if lastversion == NOTFRESH: - print _(""" - -NOTE NOTE NOTE NOTE NOTE - - You are upgrading an existing Mailman installation, but I can't tell what - version you were previously running. - - If you are upgrading from Mailman 1.0b9 or earlier you will need to - manually update your mailing lists. For each mailing list you need to - copy the file templates/options.html lists//options.html. - - However, if you have edited this file via the Web interface, you will have - to merge your changes into this file, otherwise you will lose your - changes. - -NOTE NOTE NOTE NOTE NOTE - -""") - if not errors: - # Record the version we just upgraded to - fp = open(os.path.join(config.DATA_DIR, 'last_mailman_version'), 'w') - fp.write(hex(config.HEX_VERSION) + '\n') - fp.close() - else: - lockdir = config.LOCK_DIR - print _('''\ - -ERROR: - -The locks for some lists could not be acquired. This means that either -Mailman was still active when you upgraded, or there were stale locks in the -$lockdir directory. - -You must put Mailman into a quiescent state and remove all stale locks, then -re-run "make update" manually. See the INSTALL and UPGRADE files for details. -''') diff --git a/mailman/bin/version.py b/mailman/bin/version.py deleted file mode 100644 index 0fb2c5a5b..000000000 --- a/mailman/bin/version.py +++ /dev/null @@ -1,46 +0,0 @@ -# Copyright (C) 1998-2009 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 . - -import optparse - -from mailman import version -from mailman.i18n import _ - - - -def parseargs(): - parser = optparse.OptionParser(version=version.MAILMAN_VERSION, - usage=_("""\ -%prog - -Print the Mailman version and exit.""")) - opts, args = parser.parse_args() - if args: - parser.error(_('Unexpected arguments')) - return parser, opts, args - - - -def main(): - parser, opts, args = parseargs() - # Yes, this is kind of silly - print _('Using $version.MAILMAN_VERSION ($version.CODENAME)') - - - -if __name__ == '__main__': - main() diff --git a/mailman/bin/withlist.py b/mailman/bin/withlist.py deleted file mode 100644 index 8f2d8a2b5..000000000 --- a/mailman/bin/withlist.py +++ /dev/null @@ -1,220 +0,0 @@ -# Copyright (C) 1998-2009 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 . - -import os -import sys -import optparse - -from mailman import interact -from mailman.config import config -from mailman.core.initialize import initialize -from mailman.i18n import _ -from mailman.version import MAILMAN_VERSION - - -LAST_MLIST = None -VERBOSE = True - - - -def do_list(listname, args, func): - global LAST_MLIST - - if '@' not in listname: - listname += '@' + config.DEFAULT_EMAIL_HOST - - # XXX FIXME Remove this when this script is converted to - # MultipleMailingListOptions. - listname = listname.decode(sys.getdefaultencoding()) - mlist = config.db.list_manager.get(listname) - if mlist is None: - print >> sys.stderr, _('Unknown list: $listname') - else: - if VERBOSE: - print >> sys.stderr, _('Loaded list: $listname') - LAST_MLIST = mlist - # Try to import the module and run the callable. - if func: - return func(mlist, *args) - return None - - - -def parseargs(): - parser = optparse.OptionParser(version=MAILMAN_VERSION, - usage=_("""\ -%prog [options] listname [args ...] - -General framework for interacting with a mailing list object. - -There are two ways to use this script: interactively or programmatically. -Using it interactively allows you to play with, examine and modify a -IMailinglist object from Python's interactive interpreter. When running -interactively, a IMailingList object called 'm' will be available in the -global namespace. - -Programmatically, you can write a function to operate on a IMailingList -object, and this script will take care of the housekeeping (see below for -examples). In that case, the general usage syntax is: - - % bin/withlist [options] listname [args ...] - -Here's an example of how to use the -r option. Say you have a file in the -Mailman installation directory called 'listaddr.py', with the following -two functions: - - def listaddr(mlist): - print mlist.posting_address - - def requestaddr(mlist): - print mlist.request_address - -Now, from the command line you can print the list's posting address by running -the following from the command line: - - % bin/withlist -r listaddr mylist - Loading list: mylist - Importing listaddr ... - Running listaddr.listaddr() ... - mylist@myhost.com - -And you can print the list's request address by running: - - % bin/withlist -r listaddr.requestaddr mylist - Loading list: mylist - Importing listaddr ... - Running listaddr.requestaddr() ... - mylist-request@myhost.com - -As another example, say you wanted to change the password for a particular -user on a particular list. You could put the following function in a file -called 'changepw.py': - - from mailman.errors import NotAMemberError - - def changepw(mlist, addr, newpasswd): - try: - mlist.setMemberPassword(addr, newpasswd) - mlist.Save() - except NotAMemberError: - print 'No address matched:', addr - -and run this from the command line: - - % bin/withlist -l -r changepw mylist somebody@somewhere.org foobar""")) - parser.add_option('-i', '--interactive', - default=None, action='store_true', help=_("""\ -Leaves you at an interactive prompt after all other processing is complete. -This is the default unless the -r option is given.""")) - parser.add_option('-r', '--run', - type='string', help=_("""\ -This can be used to run a script with the opened IMailingList object. This -works by attempting to import'module' (which must be in the directory -containing withlist, or already be accessible on your sys.path), and then -calling 'callable' from the module. callable can be a class or function; it -is called with the IMailingList object as the first argument. If additional -args are given on the command line, they are passed as subsequent positional -args to the callable. - -Note that 'module.' is optional; if it is omitted then a module with the name -'callable' will be imported. - -The global variable 'r' will be set to the results of this call.""")) - parser.add_option('-a', '--all', - default=False, action='store_true', help=_("""\ -This option only works with the -r option. Use this if you want to execute -the script on all mailing lists. When you use -a you should not include a -listname argument on the command line. The variable 'r' will be a list of all -the results.""")) - parser.add_option('-q', '--quiet', - default=False, action='store_true', - help=_('Suppress all status messages.')) - parser.add_option('-C', '--config', - help=_('Alternative configuration file to use')) - opts, args = parser.parse_args() - return parser, opts, args - - - -def main(): - global VERBOSE - - parser, opts, args = parseargs() - config_file = (os.getenv('MAILMAN_CONFIG_FILE') - if opts.config is None - else opts.config) - initialize(config_file, not opts.quiet) - - VERBOSE = not opts.quiet - # The default for interact is true unless -r was given - if opts.interactive is None: - if not opts.run: - opts.interactive = True - else: - opts.interactive = False - - dolist = True - if len(args) < 1 and not opts.all: - warning = _('No list name supplied.') - if opts.interactive: - # Let them keep going - print >> sys.stderr, warning - dolist = False - else: - parser.error(warning) - - if opts.all and not opts.run: - parser.error(_('--all requires --run')) - - # Try to import the module for the callable - func = None - if opts.run: - i = opts.run.rfind('.') - if i < 0: - module = opts.run - callable = opts.run - else: - module = opts.run[:i] - callable = opts.run[i+1:] - if VERBOSE: - print >> sys.stderr, _('Importing $module ...') - __import__(module) - mod = sys.modules[module] - if VERBOSE: - print >> sys.stderr, _('Running ${module}.${callable}() ...') - func = getattr(mod, callable) - - r = None - if opts.all: - r = [do_list(listname, args, func) - for listname in config.list_manager.names] - elif dolist: - listname = args.pop(0).lower().strip() - r = do_list(listname, args, func) - - # Now go to interactive mode, perhaps - if opts.interactive: - if dolist: - banner = _( - "The variable 'm' is the $listname mailing list") - else: - banner = interact.DEFAULT_BANNER - overrides = dict(m=LAST_MLIST, r=r, - commit=config.db.commit, - abort=config.db.abort, - config=config) - interact.interact(upframe=False, banner=banner, overrides=overrides) diff --git a/mailman/chains/__init__.py b/mailman/chains/__init__.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/mailman/chains/accept.py b/mailman/chains/accept.py deleted file mode 100644 index bd47f42c8..000000000 --- a/mailman/chains/accept.py +++ /dev/null @@ -1,58 +0,0 @@ -# Copyright (C) 2007-2009 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 . - -"""The terminal 'accept' chain.""" - -from __future__ import absolute_import, unicode_literals - -__metaclass__ = type -__all__ = [ - 'AcceptChain', - ] - -import logging - -from mailman.chains.base import TerminalChainBase -from mailman.config import config -from mailman.i18n import _ - - -log = logging.getLogger('mailman.vette') -SEMISPACE = '; ' - - - -class AcceptChain(TerminalChainBase): - """Accept the message for posting.""" - - name = 'accept' - description = _('Accept a message.') - - def _process(self, mlist, msg, msgdata): - """See `TerminalChainBase`.""" - # Start by decorating the message with a header that contains a list - # of all the rules that matched. These metadata could be None or an - # empty list. - rule_hits = msgdata.get('rule_hits') - if rule_hits: - msg['X-Mailman-Rule-Hits'] = SEMISPACE.join(rule_hits) - rule_misses = msgdata.get('rule_misses') - if rule_misses: - msg['X-Mailman-Rule-Misses'] = SEMISPACE.join(rule_misses) - accept_queue = config.switchboards['pipeline'] - accept_queue.enqueue(msg, msgdata) - log.info('ACCEPT: %s', msg.get('message-id', 'n/a')) diff --git a/mailman/chains/base.py b/mailman/chains/base.py deleted file mode 100644 index bcd946b40..000000000 --- a/mailman/chains/base.py +++ /dev/null @@ -1,122 +0,0 @@ -# Copyright (C) 2008-2009 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 . - -"""Base class for terminal chains.""" - -from __future__ import absolute_import, unicode_literals - -__metaclass__ = type -__all__ = [ - 'Chain', - 'Link', - 'TerminalChainBase', - ] - - -from zope.interface import implements - -from mailman.config import config -from mailman.interfaces.chain import ( - IChain, IChainIterator, IChainLink, IMutableChain, LinkAction) - - - -class Link: - """A chain link.""" - implements(IChainLink) - - def __init__(self, rule, action=None, chain=None, function=None): - self.rule = rule - self.action = (LinkAction.defer if action is None else action) - self.chain = chain - self.function = function - - - -class TerminalChainBase: - """A base chain that always matches and executes a method. - - The method is called 'process' and must be provided by the subclass. - """ - implements(IChain, IChainIterator) - - def _process(self, mlist, msg, msgdata): - """Process the message for the given mailing list. - - This must be overridden by subclasses. - """ - raise NotImplementedError - - def get_links(self, mlist, msg, msgdata): - """See `IChain`.""" - return iter(self) - - def __iter__(self): - """See `IChainIterator`.""" - truth = config.rules['truth'] - # First, yield a link that always runs the process method. - yield Link(truth, LinkAction.run, function=self._process) - # Now yield a rule that stops all processing. - yield Link(truth, LinkAction.stop) - - - -class Chain: - """Generic chain base class.""" - implements(IMutableChain) - - def __init__(self, name, description): - assert name not in config.chains, ( - 'Duplicate chain name: {0}'.format(name)) - self.name = name - self.description = description - self._links = [] - # Register the chain. - config.chains[name] = self - - def append_link(self, link): - """See `IMutableChain`.""" - self._links.append(link) - - def flush(self): - """See `IMutableChain`.""" - self._links = [] - - def get_links(self, mlist, msg, msgdata): - """See `IChain`.""" - return iter(ChainIterator(self)) - - def get_iterator(self): - """Return an iterator over the links.""" - # We do it this way in order to preserve a separation of interfaces, - # and allows .get_links() to be overridden. - for link in self._links: - yield link - - - -class ChainIterator: - """Generic chain iterator.""" - - implements(IChainIterator) - - def __init__(self, chain): - self._chain = chain - - def __iter__(self): - """See `IChainIterator`.""" - return self._chain.get_iterator() diff --git a/mailman/chains/builtin.py b/mailman/chains/builtin.py deleted file mode 100644 index 05912a2f2..000000000 --- a/mailman/chains/builtin.py +++ /dev/null @@ -1,86 +0,0 @@ -# Copyright (C) 2007-2009 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 . - -"""The default built-in starting chain.""" - -from __future__ import absolute_import, unicode_literals - -__metaclass__ = type -__all__ = [ - 'BuiltInChain', - ] - - -import logging - -from zope.interface import implements - -from mailman.chains.base import Link -from mailman.config import config -from mailman.i18n import _ -from mailman.interfaces.chain import IChain, LinkAction - - -log = logging.getLogger('mailman.vette') - - - -class BuiltInChain: - """Default built-in chain.""" - - implements(IChain) - - name = 'built-in' - description = _('The built-in moderation chain.') - - _link_descriptions = ( - ('approved', LinkAction.jump, 'accept'), - ('emergency', LinkAction.jump, 'hold'), - ('loop', LinkAction.jump, 'discard'), - # Do all of the following before deciding whether to hold the message - # for moderation. - ('administrivia', LinkAction.defer, None), - ('implicit-dest', LinkAction.defer, None), - ('max-recipients', LinkAction.defer, None), - ('max-size', LinkAction.defer, None), - ('news-moderation', LinkAction.defer, None), - ('no-subject', LinkAction.defer, None), - ('suspicious-header', LinkAction.defer, None), - # Now if any of the above hit, jump to the hold chain. - ('any', LinkAction.jump, 'hold'), - # Take a detour through the self header matching chain, which we'll - # create later. - ('truth', LinkAction.detour, 'header-match'), - # Finally, the builtin chain selfs to acceptance. - ('truth', LinkAction.jump, 'accept'), - ) - - def __init__(self): - self._cached_links = None - - def get_links(self, mlist, msg, msgdata): - """See `IChain`.""" - if self._cached_links is None: - self._cached_links = links = [] - for rule_name, action, chain_name in self._link_descriptions: - # Get the named rule. - rule = config.rules[rule_name] - # Get the chain, if one is defined. - chain = (None if chain_name is None - else config.chains[chain_name]) - links.append(Link(rule, action, chain)) - return iter(self._cached_links) diff --git a/mailman/chains/discard.py b/mailman/chains/discard.py deleted file mode 100644 index 1899e0340..000000000 --- a/mailman/chains/discard.py +++ /dev/null @@ -1,47 +0,0 @@ -# Copyright (C) 2007-2009 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 . - -"""The terminal 'discard' chain.""" - -from __future__ import absolute_import, unicode_literals - -__metaclass__ = type -__all__ = [ - 'DiscardChain', - ] - - -import logging - -from mailman.chains.base import TerminalChainBase -from mailman.i18n import _ - - -log = logging.getLogger('mailman.vette') - - - -class DiscardChain(TerminalChainBase): - """Discard a message.""" - - name = 'discard' - description = _('Discard a message and stop processing.') - - def _process(self, mlist, msg, msgdata): - """See `TerminalChainBase`.""" - log.info('DISCARD: %s', msg.get('message-id', 'n/a')) - # Nothing more needs to happen. diff --git a/mailman/chains/headers.py b/mailman/chains/headers.py deleted file mode 100644 index 2f85d78d0..000000000 --- a/mailman/chains/headers.py +++ /dev/null @@ -1,156 +0,0 @@ -# Copyright (C) 2007-2009 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 . - -"""The header-matching chain.""" - -from __future__ import absolute_import, unicode_literals - -__metaclass__ = type -__all__ = [ - 'HeaderMatchChain', - ] - - -import re -import logging -import itertools - -from zope.interface import implements - -from mailman.chains.base import Chain, Link -from mailman.config import config -from mailman.i18n import _ -from mailman.interfaces.chain import IChainIterator, LinkAction -from mailman.interfaces.rules import IRule - - -log = logging.getLogger('mailman.vette') - - - -def make_link(entry): - """Create a Link object. - - :param entry: a 2- or 3-tuple describing a link. If a 2-tuple, it is a - header and a pattern, and a default chain of 'hold' will be used. If - a 3-tuple, the third item is the chain name to use. - :return: an ILink. - """ - if len(entry) == 2: - header, pattern = entry - chain_name = 'hold' - elif len(entry) == 3: - header, pattern, chain_name = entry - # We don't assert that the chain exists here because the jump - # chain may not yet have been created. - else: - raise AssertionError('Bad link description: {0}'.format(entry)) - rule = HeaderMatchRule(header, pattern) - chain = config.chains[chain_name] - return Link(rule, LinkAction.jump, chain) - - - -class HeaderMatchRule: - """Header matching rule used by header-match chain.""" - implements(IRule) - - # Sequential rule counter. - _count = 1 - - def __init__(self, header, pattern): - self._header = header - self._pattern = pattern - self.name = 'header-match-{0:02}'.format(HeaderMatchRule._count) - HeaderMatchRule._count += 1 - self.description = '{0}: {1}'.format(header, pattern) - # XXX I think we should do better here, somehow recording that a - # particular header matched a particular pattern, but that gets ugly - # with RFC 2822 headers. It also doesn't match well with the rule - # name concept. For now, we just record the rather useless numeric - # rule name. I suppose we could do the better hit recording in the - # check() method, and set self.record = False. - self.record = True - - def check(self, mlist, msg, msgdata): - """See `IRule`.""" - for value in msg.get_all(self._header, []): - if re.search(self._pattern, value, re.IGNORECASE): - return True - return False - - - -class HeaderMatchChain(Chain): - """Default header matching chain. - - This could be extended by header match rules in the database. - """ - - def __init__(self): - super(HeaderMatchChain, self).__init__( - 'header-match', _('The built-in header matching chain')) - # The header match rules are not global, so don't register them. - # These are the only rules that the header match chain can execute. - self._links = [] - # Initialize header check rules with those from the global - # HEADER_MATCHES variable. - for entry in config.header_matches: - self._links.append(make_link(entry)) - # Keep track of how many global header matching rules we've seen. - # This is so the flush() method will only delete those that were added - # via extend() or append_link(). - self._permanent_link_count = len(self._links) - - def extend(self, header, pattern, chain_name='hold'): - """Extend the existing header matches. - - :param header: The case-insensitive header field name. - :param pattern: The pattern to match the header's value again. The - match is not anchored and is done case-insensitively. - :param chain: Option chain to jump to if the pattern matches any of - the named header values. If not given, the 'hold' chain is used. - """ - self._links.append(make_link((header, pattern, chain_name))) - - def flush(self): - """See `IMutableChain`.""" - del self._links[self._permanent_link_count:] - - def get_links(self, mlist, msg, msgdata): - """See `IChain`.""" - list_iterator = HeaderMatchIterator(mlist) - return itertools.chain(iter(self._links), iter(list_iterator)) - - def __iter__(self): - for link in self._links: - yield link - - - -class HeaderMatchIterator: - """An iterator of both the global and list-specific chain links.""" - - implements(IChainIterator) - - def __init__(self, mlist): - self._mlist = mlist - - def __iter__(self): - """See `IChainIterator`.""" - for entry in self._mlist.header_matches: - yield make_link(entry) diff --git a/mailman/chains/hold.py b/mailman/chains/hold.py deleted file mode 100644 index 16238a541..000000000 --- a/mailman/chains/hold.py +++ /dev/null @@ -1,178 +0,0 @@ -# Copyright (C) 2007-2009 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 . - -"""The terminal 'hold' chain.""" - -from __future__ import absolute_import, unicode_literals - -__metaclass__ = type -__all__ = [ - 'HoldChain', - ] - - -import logging - -from email.mime.message import MIMEMessage -from email.mime.text import MIMEText -from email.utils import formatdate, make_msgid -from zope.interface import implements - -from mailman import i18n -from mailman.Message import UserNotification -from mailman.Utils import maketext, oneline, wrap, GetCharSet -from mailman.app.moderator import hold_message -from mailman.app.replybot import autorespond_to_sender, can_acknowledge -from mailman.chains.base import TerminalChainBase -from mailman.config import config -from mailman.interfaces.pending import IPendable - - -log = logging.getLogger('mailman.vette') -SEMISPACE = '; ' -_ = i18n._ - - - -class HeldMessagePendable(dict): - implements(IPendable) - PEND_KEY = 'held message' - - - -class HoldChain(TerminalChainBase): - """Hold a message.""" - - name = 'hold' - description = _('Hold a message and stop processing.') - - def _process(self, mlist, msg, msgdata): - """See `TerminalChainBase`.""" - # Start by decorating the message with a header that contains a list - # of all the rules that matched. These metadata could be None or an - # empty list. - rule_hits = msgdata.get('rule_hits') - if rule_hits: - msg['X-Mailman-Rule-Hits'] = SEMISPACE.join(rule_hits) - rule_misses = msgdata.get('rule_misses') - if rule_misses: - msg['X-Mailman-Rule-Misses'] = SEMISPACE.join(rule_misses) - # Hold the message by adding it to the list's request database. - # XXX How to calculate the reason? - request_id = hold_message(mlist, msg, msgdata, None) - # Calculate a confirmation token to send to the author of the - # message. - pendable = HeldMessagePendable(type=HeldMessagePendable.PEND_KEY, - id=request_id) - token = config.db.pendings.add(pendable) - # Get the language to send the response in. If the sender is a - # member, then send it in the member's language, otherwise send it in - # the mailing list's preferred language. - sender = msg.get_sender() - member = mlist.members.get_member(sender) - language = (member.preferred_language - if member else mlist.preferred_language) - # A substitution dictionary for the email templates. - charset = GetCharSet(mlist.preferred_language) - original_subject = msg.get('subject') - if original_subject is None: - original_subject = _('(no subject)') - else: - original_subject = oneline(original_subject, charset) - substitutions = dict( - listname = mlist.fqdn_listname, - subject = original_subject, - sender = sender, - reason = 'XXX', #reason, - confirmurl = '{0}/{1}'.format(mlist.script_url('confirm'), token), - admindb_url = mlist.script_url('admindb'), - ) - # At this point the message is held, but now we have to craft at least - # two responses. The first will go to the original author of the - # message and it will contain the token allowing them to approve or - # discard the message. The second one will go to the moderators of - # the mailing list, if the list is so configured. - # - # Start by possibly sending a response to the message author. There - # are several reasons why we might not go through with this. If the - # message was gated from NNTP, the author may not even know about this - # list, so don't spam them. If the author specifically requested that - # acknowledgments not be sent, or if the message was bulk email, then - # we do not send the response. It's also possible that either the - # mailing list, or the author (if they are a member) have been - # configured to not send such responses. - if (not msgdata.get('fromusenet') and - can_acknowledge(msg) and - mlist.respond_to_post_requests and - autorespond_to_sender(mlist, sender, language)): - # We can respond to the sender with a message indicating their - # posting was held. - subject = _( - 'Your message to $mlist.fqdn_listname awaits moderator approval') - send_language = msgdata.get('lang', language) - text = maketext('postheld.txt', substitutions, - lang=send_language, mlist=mlist) - adminaddr = mlist.bounces_address - nmsg = UserNotification(sender, adminaddr, subject, text, - send_language) - nmsg.send(mlist) - # Now the message for the list moderators. This one should appear to - # come from -owner since we really don't need to do bounce - # processing on it. - if mlist.admin_immed_notify: - # Now let's temporarily set the language context to that which the - # administrators are expecting. - with i18n.using_language(mlist.preferred_language): - language = mlist.preferred_language - charset = GetCharSet(language) - # We need to regenerate or re-translate a few values in the - # substitution dictionary. - #d['reason'] = _(reason) # XXX reason - substitutions['subject'] = original_subject - # craft the admin notification message and deliver it - subject = _( - '$mlist.fqdn_listname post from $sender requires approval') - nmsg = UserNotification(mlist.owner_address, - mlist.owner_address, - subject, lang=language) - nmsg.set_type('multipart/mixed') - text = MIMEText( - maketext('postauth.txt', substitutions, - raw=True, mlist=mlist), - _charset=charset) - dmsg = MIMEText(wrap(_("""\ -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.""")), - _charset=GetCharSet(language)) - dmsg['Subject'] = 'confirm ' + token - dmsg['Sender'] = mlist.request_address - dmsg['From'] = mlist.request_address - dmsg['Date'] = formatdate(localtime=True) - dmsg['Message-ID'] = make_msgid() - nmsg.attach(text) - nmsg.attach(MIMEMessage(msg)) - nmsg.attach(MIMEMessage(dmsg)) - nmsg.send(mlist, **dict(tomoderators=True)) - # Log the held message - # XXX reason - reason = 'n/a' - log.info('HOLD: %s post from %s held, message-id=%s: %s', - mlist.fqdn_listname, sender, - msg.get('message-id', 'n/a'), reason) diff --git a/mailman/chains/reject.py b/mailman/chains/reject.py deleted file mode 100644 index 3faf563da..000000000 --- a/mailman/chains/reject.py +++ /dev/null @@ -1,59 +0,0 @@ -# Copyright (C) 2007-2009 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 . - -"""The terminal 'reject' chain.""" - -from __future__ import absolute_import, unicode_literals - -__metaclass__ = type -__all__ = [ - 'RejectChain', - ] - - -import logging - -from mailman.app.bounces import bounce_message -from mailman.chains.base import TerminalChainBase -from mailman.i18n import _ - - -log = logging.getLogger('mailman.vette') -SEMISPACE = '; ' - - - -class RejectChain(TerminalChainBase): - """Reject/bounce a message.""" - - name = 'reject' - description = _('Reject/bounce a message and stop processing.') - - def _process(self, mlist, msg, msgdata): - """See `TerminalChainBase`.""" - # Start by decorating the message with a header that contains a list - # of all the rules that matched. These metadata could be None or an - # empty list. - rule_hits = msgdata.get('rule_hits') - if rule_hits: - msg['X-Mailman-Rule-Hits'] = SEMISPACE.join(rule_hits) - rule_misses = msgdata.get('rule_misses') - if rule_misses: - msg['X-Mailman-Rule-Misses'] = SEMISPACE.join(rule_misses) - # XXX Exception/reason - bounce_message(mlist, msg) - log.info('REJECT: %s', msg.get('message-id', 'n/a')) diff --git a/mailman/commands/__init__.py b/mailman/commands/__init__.py deleted file mode 100644 index 6e89bc6da..000000000 --- a/mailman/commands/__init__.py +++ /dev/null @@ -1,22 +0,0 @@ -# Copyright (C) 2008-2009 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 . - -__all__ = [ - 'echo', - 'end', - 'join', - ] diff --git a/mailman/commands/cmd_confirm.py b/mailman/commands/cmd_confirm.py deleted file mode 100644 index b5e4182bd..000000000 --- a/mailman/commands/cmd_confirm.py +++ /dev/null @@ -1,98 +0,0 @@ -# Copyright (C) 2002-2009 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 . - -""" - confirm - Confirm an action. The confirmation-string is required and should be - supplied by a mailback confirmation notice. -""" - -from mailman import Errors -from mailman import Pending -from mailman.config import config -from mailman.i18n import _ - -STOP = 1 - - - -def gethelp(mlist): - return _(__doc__) - - - -def process(res, args): - mlist = res.mlist - if len(args) <> 1: - res.results.append(_('Usage:')) - res.results.append(gethelp(mlist)) - return STOP - cookie = args[0] - try: - results = mlist.ProcessConfirmation(cookie, res.msg) - except Errors.MMBadConfirmation, e: - # Express in approximate days - days = int(config.PENDING_REQUEST_LIFE / config.days(1) + 0.5) - res.results.append(_("""\ -Invalid confirmation string. Note that confirmation strings expire -approximately %(days)s days after the initial subscription request. If your -confirmation has expired, please try to re-submit your original request or -message.""")) - except Errors.MMNeedApproval: - res.results.append(_("""\ -Your request has been forwarded to the list moderator for approval.""")) - except Errors.MMAlreadyAMember: - # Some other subscription request for this address has - # already succeeded. - res.results.append(_('You are already subscribed.')) - except Errors.NotAMemberError: - # They've already been unsubscribed - res.results.append(_("""\ -You are not currently a member. Have you already unsubscribed or changed -your email address?""")) - except Errors.MembershipIsBanned: - owneraddr = mlist.GetOwnerEmail() - res.results.append(_("""\ -You are currently banned from subscribing to this list. If you think this -restriction is erroneous, please contact the list owners at -%(owneraddr)s.""")) - except Errors.HostileSubscriptionError: - res.results.append(_("""\ -You were not invited to this mailing list. The invitation has been discarded, -and both list administrators have been alerted.""")) - except Errors.MMBadPasswordError: - res.results.append(_("""\ -Bad approval password given. Held message is still being held.""")) - else: - if ((results[0] == Pending.SUBSCRIPTION and mlist.send_welcome_msg) - or - (results[0] == Pending.UNSUBSCRIPTION and mlist.send_goodbye_msg)): - # We don't also need to send a confirmation succeeded message - res.respond = 0 - else: - res.results.append(_('Confirmation succeeded')) - # Consume any other confirmation strings with the same cookie so - # the user doesn't get a misleading "unprocessed" message. - match = 'confirm ' + cookie - unprocessed = [] - for line in res.commands: - if line.lstrip() == match: - continue - unprocessed.append(line) - res.commands = unprocessed - # Process just one confirmation string per message - return STOP diff --git a/mailman/commands/cmd_help.py b/mailman/commands/cmd_help.py deleted file mode 100644 index eeee33ca7..000000000 --- a/mailman/commands/cmd_help.py +++ /dev/null @@ -1,93 +0,0 @@ -# Copyright (C) 2002-2009 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 . - -""" - help - Print this help message. -""" - -import os -import sys - -from mailman import Utils -from mailman.config import config -from mailman.i18n import _ - -EMPTYSTRING = '' - - - -def gethelp(mlist): - return _(__doc__) - - - -def process(res, args): - # Get the help text introduction - mlist = res.mlist - # Since this message is personalized, add some useful information if the - # address requesting help is a member of the list. - msg = res.msg - for sender in msg.get_senders(): - if mlist.isMember(sender): - memberurl = mlist.GetOptionsURL(sender, absolute=1) - urlhelp = _( - 'You can access your personal options via the following url:') - res.results.append(urlhelp) - res.results.append(memberurl) - # Get a blank line in the output. - res.results.append('') - break - # build the specific command helps from the module docstrings - modhelps = {} - import mailman.Commands - path = os.path.dirname(os.path.abspath(mailman.Commands.__file__)) - for file in os.listdir(path): - if not file.startswith('cmd_') or not file.endswith('.py'): - continue - module = os.path.splitext(file)[0] - modname = 'mailman.Commands.' + module - try: - __import__(modname) - except ImportError: - continue - cmdname = module[4:] - help = None - if hasattr(sys.modules[modname], 'gethelp'): - help = sys.modules[modname].gethelp(mlist) - if help: - modhelps[cmdname] = help - # Now sort the command helps - helptext = [] - keys = modhelps.keys() - keys.sort() - for cmd in keys: - helptext.append(modhelps[cmd]) - commands = EMPTYSTRING.join(helptext) - # Now craft the response - helptext = Utils.maketext( - 'help.txt', - {'listname' : mlist.real_name, - 'version' : config.VERSION, - 'listinfo_url': mlist.GetScriptURL('listinfo', absolute=1), - 'requestaddr' : mlist.GetRequestEmail(), - 'adminaddr' : mlist.GetOwnerEmail(), - 'commands' : commands, - }, mlist=mlist, lang=res.msgdata['lang'], raw=1) - # Now add to the response - res.results.append('help') - res.results.append(helptext) diff --git a/mailman/commands/cmd_info.py b/mailman/commands/cmd_info.py deleted file mode 100644 index 3bdea178f..000000000 --- a/mailman/commands/cmd_info.py +++ /dev/null @@ -1,50 +0,0 @@ -# Copyright (C) 2002-2009 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 . - -""" - info - Get information about this mailing list. -""" - -from mailman.i18n import _ - -STOP = 1 - - - -def gethelp(mlist): - return _(__doc__) - - - -def process(res, args): - mlist = res.mlist - if args: - res.results.append(gethelp(mlist)) - return STOP - listname = mlist.real_name - description = mlist.description or _('n/a') - postaddr = mlist.posting_address - requestaddr = mlist.request_address - owneraddr = mlist.owner_address - listurl = mlist.script_url('listinfo') - res.results.append(_('List name: %(listname)s')) - res.results.append(_('Description: %(description)s')) - res.results.append(_('Postings to: %(postaddr)s')) - res.results.append(_('List Helpbot: %(requestaddr)s')) - res.results.append(_('List Owners: %(owneraddr)s')) - res.results.append(_('More information: %(listurl)s')) diff --git a/mailman/commands/cmd_leave.py b/mailman/commands/cmd_leave.py deleted file mode 100644 index 5844824f7..000000000 --- a/mailman/commands/cmd_leave.py +++ /dev/null @@ -1,21 +0,0 @@ -# Copyright (C) 2002-2009 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 . - -"""The `leave' command is synonymous with `unsubscribe'. -""" - -from mailman.Commands.cmd_unsubscribe import process diff --git a/mailman/commands/cmd_lists.py b/mailman/commands/cmd_lists.py deleted file mode 100644 index 234ef46fc..000000000 --- a/mailman/commands/cmd_lists.py +++ /dev/null @@ -1,65 +0,0 @@ -# Copyright (C) 2002-2009 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 . - -""" - lists - See a list of the public mailing lists on this GNU Mailman server. -""" - -from mailman.MailList import MailList -from mailman.config import config -from mailman.i18n import _ - - -STOP = 1 - - - -def gethelp(mlist): - return _(__doc__) - - - -def process(res, args): - mlist = res.mlist - if args: - res.results.append(_('Usage:')) - res.results.append(gethelp(mlist)) - return STOP - hostname = mlist.host_name - res.results.append(_('Public mailing lists at %(hostname)s:')) - i = 1 - for listname in sorted(config.list_manager.names): - if listname == mlist.internal_name(): - xlist = mlist - else: - xlist = MailList(listname, lock=0) - # We can mention this list if you already know about it - if not xlist.advertised and xlist is not mlist: - continue - # Skip the list if it isn't in the same virtual domain. - if xlist.host_name <> mlist.host_name: - continue - realname = xlist.real_name - description = xlist.description or _('n/a') - requestaddr = xlist.GetRequestEmail() - if i > 1: - res.results.append('') - res.results.append(_('%(i)3d. List name: %(realname)s')) - res.results.append(_(' Description: %(description)s')) - res.results.append(_(' Requests to: %(requestaddr)s')) - i += 1 diff --git a/mailman/commands/cmd_password.py b/mailman/commands/cmd_password.py deleted file mode 100644 index 545da0cb5..000000000 --- a/mailman/commands/cmd_password.py +++ /dev/null @@ -1,123 +0,0 @@ -# Copyright (C) 2002-2009 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 . - -""" - password [ ] [address=
      ] - Retrieve or change your password. With no arguments, this returns - your current password. With arguments and - you can change your password. - - If you're posting from an address other than your membership address, - specify your membership address with `address=
      ' (no brackets - around the email address, and no quotes!). Note that in this case the - response is always sent to the subscribed address. -""" - -from email.Utils import parseaddr - -from mailman.config import config -from mailman.i18n import _ - -STOP = 1 - - - -def gethelp(mlist): - return _(__doc__) - - - -def process(res, args): - mlist = res.mlist - address = None - if not args: - # They just want to get their existing password - realname, address = parseaddr(res.msg['from']) - if mlist.isMember(address): - password = mlist.getMemberPassword(address) - res.results.append(_('Your password is: %(password)s')) - # Prohibit multiple password retrievals. - return STOP - else: - listname = mlist.real_name - res.results.append( - _('You are not a member of the %(listname)s mailing list')) - return STOP - elif len(args) == 1 and args[0].startswith('address='): - # They want their password, but they're posting from a different - # address. We /must/ return the password to the subscribed address. - address = args[0][8:] - res.returnaddr = address - if mlist.isMember(address): - password = mlist.getMemberPassword(address) - res.results.append(_('Your password is: %(password)s')) - # Prohibit multiple password retrievals. - return STOP - else: - listname = mlist.real_name - res.results.append( - _('You are not a member of the %(listname)s mailing list')) - return STOP - elif len(args) == 2: - # They are changing their password - oldpasswd = args[0] - newpasswd = args[1] - realname, address = parseaddr(res.msg['from']) - if mlist.isMember(address): - if mlist.Authenticate((config.AuthUser, config.AuthListAdmin), - oldpasswd, address): - mlist.setMemberPassword(address, newpasswd) - res.results.append(_('Password successfully changed.')) - else: - res.results.append(_("""\ -You did not give the correct old password, so your password has not been -changed. Use the no argument version of the password command to retrieve your -current password, then try again.""")) - res.results.append(_('\nUsage:')) - res.results.append(gethelp(mlist)) - return STOP - else: - listname = mlist.real_name - res.results.append( - _('You are not a member of the %(listname)s mailing list')) - return STOP - elif len(args) == 3 and args[2].startswith('address='): - # They want to change their password, and they're sending this from a - # different address than what they're subscribed with. Be sure the - # response goes to the subscribed address. - oldpasswd = args[0] - newpasswd = args[1] - address = args[2][8:] - res.returnaddr = address - if mlist.isMember(address): - if mlist.Authenticate((config.AuthUser, config.AuthListAdmin), - oldpasswd, address): - mlist.setMemberPassword(address, newpasswd) - res.results.append(_('Password successfully changed.')) - else: - res.results.append(_("""\ -You did not give the correct old password, so your password has not been -changed. Use the no argument version of the password command to retrieve your -current password, then try again.""")) - res.results.append(_('\nUsage:')) - res.results.append(gethelp(mlist)) - return STOP - else: - listname = mlist.real_name - res.results.append( - _('You are not a member of the %(listname)s mailing list')) - return STOP diff --git a/mailman/commands/cmd_remove.py b/mailman/commands/cmd_remove.py deleted file mode 100644 index 8f3ce9669..000000000 --- a/mailman/commands/cmd_remove.py +++ /dev/null @@ -1,21 +0,0 @@ -# Copyright (C) 2002-2009 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 . - -"""The `remove' command is synonymous with `unsubscribe'. -""" - -from mailman.Commands.cmd_unsubscribe import process diff --git a/mailman/commands/cmd_set.py b/mailman/commands/cmd_set.py deleted file mode 100644 index 020bc3636..000000000 --- a/mailman/commands/cmd_set.py +++ /dev/null @@ -1,360 +0,0 @@ -# Copyright (C) 2002-2009 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 . - -from email.Utils import parseaddr, formatdate - -from mailman import Errors -from mailman import MemberAdaptor -from mailman import i18n -from mailman.config import config - -def _(s): return s - -OVERVIEW = _(""" - set ... - Set or view your membership options. - - Use `set help' (without the quotes) to get a more detailed list of the - options you can change. - - Use `set show' (without the quotes) to view your current option - settings. -""") - -DETAILS = _(""" - set help - Show this detailed help. - - set show [address=
      ] - View your current option settings. If you're posting from an address - other than your membership address, specify your membership address - with `address=
      ' (no brackets around the email address, and no - quotes!). - - set authenticate [address=
      ] - To set any of your options, you must include this command first, along - with your membership password. If you're posting from an address - other than your membership address, specify your membership address - with `address=
      ' (no brackets around the email address, and no - quotes!). - - set ack on - set ack off - When the `ack' option is turned on, you will receive an - acknowledgement message whenever you post a message to the list. - - set digest plain - set digest mime - set digest off - When the `digest' option is turned off, you will receive postings - immediately when they are posted. Use `set digest plain' if instead - you want to receive postings bundled into a plain text digest - (i.e. RFC 1153 digest). Use `set digest mime' if instead you want to - receive postings bundled together into a MIME digest. - - set delivery on - set delivery off - Turn delivery on or off. This does not unsubscribe you, but instead - tells Mailman not to deliver messages to you for now. This is useful - if you're going on vacation. Be sure to use `set delivery on' when - you return from vacation! - - set myposts on - set myposts off - Use `set myposts off' to not receive copies of messages you post to - the list. This has no effect if you're receiving digests. - - set hide on - set hide off - Use `set hide on' to conceal your email address when people request - the membership list. - - set duplicates on - set duplicates off - Use `set duplicates off' if you want Mailman to not send you messages - if your address is explicitly mentioned in the To: or Cc: fields of - the message. This can reduce the number of duplicate postings you - will receive. - - set reminders on - set reminders off - Use `set reminders off' if you want to disable the monthly password - reminder for this mailing list. -""") - -_ = i18n._ - -STOP = 1 - - - -def gethelp(mlist): - return _(OVERVIEW) - - - -class SetCommands: - def __init__(self): - self.__address = None - self.__authok = 0 - - def process(self, res, args): - if not args: - res.results.append(_(DETAILS)) - return STOP - subcmd = args.pop(0) - methname = 'set_' + subcmd - method = getattr(self, methname, None) - if method is None: - res.results.append(_('Bad set command: %(subcmd)s')) - res.results.append(_(DETAILS)) - return STOP - return method(res, args) - - def set_help(self, res, args=1): - res.results.append(_(DETAILS)) - if args: - return STOP - - def _usage(self, res): - res.results.append(_('Usage:')) - return self.set_help(res) - - def set_show(self, res, args): - mlist = res.mlist - if not args: - realname, address = parseaddr(res.msg['from']) - elif len(args) == 1 and args[0].startswith('address='): - # Send the results to the address, not the From: dude - address = args[0][8:] - res.returnaddr = address - else: - return self._usage(res) - if not mlist.isMember(address): - listname = mlist.real_name - res.results.append( - _('You are not a member of the %(listname)s mailing list')) - return STOP - res.results.append(_('Your current option settings:')) - opt = mlist.getMemberOption(address, config.AcknowledgePosts) - onoff = opt and _('on') or _('off') - res.results.append(_(' ack %(onoff)s')) - # Digests are a special ternary value - digestsp = mlist.getMemberOption(address, config.Digests) - if digestsp: - plainp = mlist.getMemberOption(address, config.DisableMime) - if plainp: - res.results.append(_(' digest plain')) - else: - res.results.append(_(' digest mime')) - else: - res.results.append(_(' digest off')) - # If their membership is disabled, let them know why - status = mlist.getDeliveryStatus(address) - how = None - if status == MemberAdaptor.ENABLED: - status = _('delivery on') - elif status == MemberAdaptor.BYUSER: - status = _('delivery off') - how = _('by you') - elif status == MemberAdaptor.BYADMIN: - status = _('delivery off') - how = _('by the admin') - elif status == MemberAdaptor.BYBOUNCE: - status = _('delivery off') - how = _('due to bounces') - else: - assert status == MemberAdaptor.UNKNOWN - status = _('delivery off') - how = _('for unknown reasons') - changetime = mlist.getDeliveryStatusChangeTime(address) - if how and changetime > 0: - date = formatdate(changetime) - res.results.append(_(' %(status)s (%(how)s on %(date)s)')) - else: - res.results.append(' ' + status) - opt = mlist.getMemberOption(address, config.DontReceiveOwnPosts) - # sense is reversed - onoff = (not opt) and _('on') or _('off') - res.results.append(_(' myposts %(onoff)s')) - opt = mlist.getMemberOption(address, config.ConcealSubscription) - onoff = opt and _('on') or _('off') - res.results.append(_(' hide %(onoff)s')) - opt = mlist.getMemberOption(address, config.DontReceiveDuplicates) - # sense is reversed - onoff = (not opt) and _('on') or _('off') - res.results.append(_(' duplicates %(onoff)s')) - opt = mlist.getMemberOption(address, config.SuppressPasswordReminder) - # sense is reversed - onoff = (not opt) and _('on') or _('off') - res.results.append(_(' reminders %(onoff)s')) - - def set_authenticate(self, res, args): - mlist = res.mlist - if len(args) == 1: - realname, address = parseaddr(res.msg['from']) - password = args[0] - elif len(args) == 2 and args[1].startswith('address='): - password = args[0] - address = args[1][8:] - else: - return self._usage(res) - # See if the password matches - if not mlist.isMember(address): - listname = mlist.real_name - res.results.append( - _('You are not a member of the %(listname)s mailing list')) - return STOP - if not mlist.Authenticate((config.AuthUser, - config.AuthListAdmin), - password, address): - res.results.append(_('You did not give the correct password')) - return STOP - self.__authok = 1 - self.__address = address - - def _status(self, res, arg): - status = arg.lower() - if status == 'on': - flag = 1 - elif status == 'off': - flag = 0 - else: - res.results.append(_('Bad argument: %(arg)s')) - self._usage(res) - return -1 - # See if we're authenticated - if not self.__authok: - res.results.append(_('Not authenticated')) - self._usage(res) - return -1 - return flag - - def set_ack(self, res, args): - mlist = res.mlist - if len(args) <> 1: - return self._usage(res) - status = self._status(res, args[0]) - if status < 0: - return STOP - mlist.setMemberOption(self.__address, config.AcknowledgePosts, status) - res.results.append(_('ack option set')) - - def set_digest(self, res, args): - mlist = res.mlist - if len(args) <> 1: - return self._usage(res) - if not self.__authok: - res.results.append(_('Not authenticated')) - self._usage(res) - return STOP - arg = args[0].lower() - if arg == 'off': - try: - mlist.setMemberOption(self.__address, config.Digests, 0) - except Errors.AlreadyReceivingRegularDeliveries: - pass - elif arg == 'plain': - try: - mlist.setMemberOption(self.__address, config.Digests, 1) - except Errors.AlreadyReceivingDigests: - pass - mlist.setMemberOption(self.__address, config.DisableMime, 1) - elif arg == 'mime': - try: - mlist.setMemberOption(self.__address, config.Digests, 1) - except Errors.AlreadyReceivingDigests: - pass - mlist.setMemberOption(self.__address, config.DisableMime, 0) - else: - res.results.append(_('Bad argument: %(arg)s')) - self._usage(res) - return STOP - res.results.append(_('digest option set')) - - def set_delivery(self, res, args): - mlist = res.mlist - if len(args) <> 1: - return self._usage(res) - status = self._status(res, args[0]) - if status < 0: - return STOP - # Delivery status is handled differently than other options. If - # status is true (set delivery on), then we enable delivery. - # Otherwise, we have to use the setDeliveryStatus() interface to - # specify that delivery was disabled by the user. - if status: - mlist.setDeliveryStatus(self.__address, MemberAdaptor.ENABLED) - res.results.append(_('delivery enabled')) - else: - mlist.setDeliveryStatus(self.__address, MemberAdaptor.BYUSER) - res.results.append(_('delivery disabled by user')) - - def set_myposts(self, res, args): - mlist = res.mlist - if len(args) <> 1: - return self._usage(res) - status = self._status(res, args[0]) - if status < 0: - return STOP - # sense is reversed - mlist.setMemberOption(self.__address, config.DontReceiveOwnPosts, - not status) - res.results.append(_('myposts option set')) - - def set_hide(self, res, args): - mlist = res.mlist - if len(args) <> 1: - return self._usage(res) - status = self._status(res, args[0]) - if status < 0: - return STOP - mlist.setMemberOption(self.__address, config.ConcealSubscription, - status) - res.results.append(_('hide option set')) - - def set_duplicates(self, res, args): - mlist = res.mlist - if len(args) <> 1: - return self._usage(res) - status = self._status(res, args[0]) - if status < 0: - return STOP - # sense is reversed - mlist.setMemberOption(self.__address, config.DontReceiveDuplicates, - not status) - res.results.append(_('duplicates option set')) - - def set_reminders(self, res, args): - mlist = res.mlist - if len(args) <> 1: - return self._usage(res) - status = self._status(res, args[0]) - if status < 0: - return STOP - # sense is reversed - mlist.setMemberOption(self.__address, config.SuppressPasswordReminder, - not status) - res.results.append(_('reminder option set')) - - - -def process(res, args): - # We need to keep some state between set commands - if not getattr(res, 'setstate', None): - res.setstate = SetCommands() - res.setstate.process(res, args) diff --git a/mailman/commands/cmd_unsubscribe.py b/mailman/commands/cmd_unsubscribe.py deleted file mode 100644 index 456b8089d..000000000 --- a/mailman/commands/cmd_unsubscribe.py +++ /dev/null @@ -1,88 +0,0 @@ -# Copyright (C) 2002-2009 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 . - -""" - unsubscribe [password] [address=
      ] - Unsubscribe from the mailing list. If given, your password must match - your current password. If omitted, a confirmation email will be sent - to the unsubscribing address. If you wish to unsubscribe an address - other than the address you sent this request from, you may specify - `address=
      ' (no brackets around the email address, and no - quotes!) -""" - -from email.Utils import parseaddr - -from mailman import Errors -from mailman.i18n import _ - -STOP = 1 - - - -def gethelp(mlist): - return _(__doc__) - - - -def process(res, args): - mlist = res.mlist - password = None - address = None - argnum = 0 - for arg in args: - if arg.startswith('address='): - address = arg[8:] - elif argnum == 0: - password = arg - else: - res.results.append(_('Usage:')) - res.results.append(gethelp(mlist)) - return STOP - argnum += 1 - # Fill in empty defaults - if address is None: - realname, address = parseaddr(res.msg['from']) - if not mlist.isMember(address): - listname = mlist.real_name - res.results.append( - _('%(address)s is not a member of the %(listname)s mailing list')) - return STOP - # If we're doing admin-approved unsubs, don't worry about the password - if mlist.unsubscribe_policy: - try: - mlist.DeleteMember(address, 'mailcmd') - except Errors.MMNeedApproval: - res.results.append(_("""\ -Your unsubscription request has been forwarded to the list administrator for -approval.""")) - elif password is None: - # No password was given, so we need to do a mailback confirmation - # instead of unsubscribing them here. - cpaddr = mlist.getMemberCPAddress(address) - mlist.ConfirmUnsubscription(cpaddr) - # We don't also need to send a confirmation to this command - res.respond = 0 - else: - # No admin approval is necessary, so we can just delete them if the - # passwords match. - oldpw = mlist.getMemberPassword(address) - if oldpw <> password: - res.results.append(_('You gave the wrong password')) - return STOP - mlist.ApprovedDeleteMember(address, 'mailcmd') - res.results.append(_('Unsubscription request succeeded.')) diff --git a/mailman/commands/cmd_who.py b/mailman/commands/cmd_who.py deleted file mode 100644 index 6c66610b3..000000000 --- a/mailman/commands/cmd_who.py +++ /dev/null @@ -1,152 +0,0 @@ -# Copyright (C) 2002-2009 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 . - -from email.Utils import parseaddr - -from mailman import i18n -from mailman.config import config - -STOP = 1 - -def _(s): return s - -PUBLICHELP = _(""" - who - See the non-hidden members of this mailing list. - who password - See everyone who is on this mailing list. The password is the - list's admin or moderator password. -""") - -MEMBERSONLYHELP = _(""" - who password [address=
      ] - See the non-hidden members of this mailing list. The roster is - limited to list members only, and you must supply your membership - password to retrieve it. If you're posting from an address other - than your membership address, specify your membership address with - `address=
      ' (no brackets around the email address, and no - quotes!). If you provide the list's admin or moderator password, - hidden members will be included. -""") - -ADMINONLYHELP = _(""" - who password - See everyone who is on this mailing list. The roster is limited to - list administrators and moderators only; you must supply the list - admin or moderator password to retrieve the roster. -""") - -_ = i18n._ - - - -def gethelp(mlist): - if mlist.private_roster == 0: - return _(PUBLICHELP) - elif mlist.private_roster == 1: - return _(MEMBERSONLYHELP) - elif mlist.private_roster == 2: - return _(ADMINONLYHELP) - - -def usage(res): - res.results.append(_('Usage:')) - res.results.append(gethelp(res.mlist)) - - - -def process(res, args): - mlist = res.mlist - address = None - password = None - ok = False - full = False - if mlist.private_roster == 0: - # Public rosters - if args: - if len(args) == 1: - if mlist.Authenticate((config.AuthListModerator, - config.AuthListAdmin), - args[0]): - full = True - else: - usage(res) - return STOP - else: - usage(res) - return STOP - ok = True - elif mlist.private_roster == 1: - # List members only - if len(args) == 1: - password = args[0] - realname, address = parseaddr(res.msg['from']) - elif len(args) == 2 and args[1].startswith('address='): - password = args[0] - address = args[1][8:] - else: - usage(res) - return STOP - if mlist.isMember(address) and mlist.Authenticate( - (config.AuthUser, - config.AuthListModerator, - config.AuthListAdmin), - password, address): - # Then - ok = True - if mlist.Authenticate( - (config.AuthListModerator, - config.AuthListAdmin), - password): - # Then - ok = full = True - else: - # Admin only - if len(args) <> 1: - usage(res) - return STOP - if mlist.Authenticate((config.AuthListModerator, - config.AuthListAdmin), - args[0]): - ok = full = True - if not ok: - res.results.append( - _('You are not allowed to retrieve the list membership.')) - return STOP - # It's okay for this person to see the list membership - dmembers = mlist.getDigestMemberKeys() - rmembers = mlist.getRegularMemberKeys() - if not dmembers and not rmembers: - res.results.append(_('This list has no members.')) - return - # Convenience function - def addmembers(members): - for member in members: - if not full and mlist.getMemberOption(member, - config.ConcealSubscription): - continue - realname = mlist.getMemberName(member) - if realname: - res.results.append(' %s (%s)' % (member, realname)) - else: - res.results.append(' %s' % member) - if rmembers: - res.results.append(_('Non-digest (regular) members:')) - addmembers(rmembers) - if dmembers: - res.results.append(_('Digest members:')) - addmembers(dmembers) diff --git a/mailman/commands/docs/echo.txt b/mailman/commands/docs/echo.txt deleted file mode 100644 index 181cc58c8..000000000 --- a/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]' - >>> command.description - u'Echo an acknowledgement. Arguments are return unchanged.' - -The original message is ignored, but the results receive the echoed command. - - >>> from mailman.app.lifecycle import create_list - >>> mlist = create_list(u'test@example.com') - - >>> from mailman.queue.command import Results - >>> results = Results() - - >>> from mailman.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/mailman/commands/docs/end.txt b/mailman/commands/docs/end.txt deleted file mode 100644 index 4f6af26cb..000000000 --- a/mailman/commands/docs/end.txt +++ /dev/null @@ -1,37 +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' - >>> command.description - u'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. - - >>> from mailman.app.lifecycle import create_list - >>> mlist = create_list(u'test@example.com') - >>> from mailman.Message import Message - >>> print command.process(mlist, Message(), {}, (), None) - ContinueProcessing.no - -The 'stop' command is a synonym for 'end'. - - >>> command = config.commands['stop'] - >>> command.name - 'stop' - >>> command.description - u'Stop processing commands.' - >>> command.argument_description - '' - >>> print command.process(mlist, Message(), {}, (), None) - ContinueProcessing.no diff --git a/mailman/commands/docs/join.txt b/mailman/commands/docs/join.txt deleted file mode 100644 index 9b85e816c..000000000 --- a/mailman/commands/docs/join.txt +++ /dev/null @@ -1,170 +0,0 @@ -The 'join' command -================== - -The mail command 'join' subscribes an email address to the mailing list. -'subscribe' is an alias for 'join'. - - >>> command = config.commands['join'] - >>> print command.name - join - >>> print command.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 command.argument_description - [digest=] [address=
      ] - - -No address to join ------------------- - - >>> from mailman.Message import Message - >>> from mailman.app.lifecycle import create_list - >>> from mailman.queue.command import Results - >>> mlist = create_list(u'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. - - >>> results = Results() - >>> print command.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. - - >>> subscribe = config.commands['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 command.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. - - >>> print config.db.user_manager.get_user(u'anne@example.com') - None - -Mailman has sent her the confirmation message. - - >>> virginq = config.switchboards['virgin'] - >>> qmsg, qdata = virginq.dequeue(virginq.files[0]) - >>> print qmsg.as_string() - MIME-Version: 1.0 - ... - Subject: confirm ... - From: 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. - - >>> token = str(qmsg['subject']).split()[1].strip() - >>> from mailman.interfaces.registrar import IRegistrar - >>> registrar = IRegistrar(config.domains['example.com']) - >>> registrar.confirm(token) - True - - >>> user = config.db.user_manager.get_user(u'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(u'anne@example.com') - - on alpha@example.com as MemberRole.member> - - -Joining a second list ---------------------- - - >>> mlist_2 = create_list(u'baker@example.com') - >>> msg = message_from_string("""\ - ... From: Anne Person - ... - ... """) - >>> print command.process(mlist_2, msg, {}, (), Results()) - ContinueProcessing.yes - -Anne of course, is still registered. - - >>> print config.db.user_manager.get_user(u'anne@example.com') - - -But she is not a member of the mailing list. - - >>> print mlist_2.members.get_member(u'anne@example.com') - None - -One Anne confirms this subscription, she becomes a member of the mailing list. - - >>> qmsg, qdata = virginq.dequeue(virginq.files[0]) - >>> token = str(qmsg['subject']).split()[1].strip() - >>> registrar.confirm(token) - True - - >>> print mlist_2.members.get_member(u'anne@example.com') - - on baker@example.com as MemberRole.member> diff --git a/mailman/commands/echo.py b/mailman/commands/echo.py deleted file mode 100644 index 30590acf8..000000000 --- a/mailman/commands/echo.py +++ /dev/null @@ -1,48 +0,0 @@ -# Copyright (C) 2002-2009 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 . - -"""The email command 'echo'.""" - -__metaclass__ = type -__all__ = [ - 'Echo', - ] - - -from zope.interface import implements - -from mailman.i18n import _ -from mailman.interfaces.command import ContinueProcessing, IEmailCommand - - -SPACE = ' ' - - - -class Echo: - """The email 'echo' command.""" - implements(IEmailCommand) - - name = 'echo' - argument_description = '[args]' - description = _( - 'Echo an acknowledgement. Arguments are return unchanged.') - - def process(self, mlist, msg, msgdata, arguments, results): - """See `IEmailCommand`.""" - print >> results, 'echo', SPACE.join(arguments) - return ContinueProcessing.yes diff --git a/mailman/commands/end.py b/mailman/commands/end.py deleted file mode 100644 index a9298bc92..000000000 --- a/mailman/commands/end.py +++ /dev/null @@ -1,51 +0,0 @@ -# Copyright (C) 2002-2009 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 . - -"""The email commands 'end' and 'stop'.""" - -__metaclass__ = type -__all__ = [ - 'End', - 'Stop', - ] - - -from zope.interface import implements - -from mailman.i18n import _ -from mailman.interfaces.command import ContinueProcessing, IEmailCommand - - - -class End: - """The email 'end' command.""" - implements(IEmailCommand) - - name = 'end' - argument_description = '' - description = _('Stop processing commands.') - - def process(self, mlist, msg, msgdata, arguments, results): - """See `IEmailCommand`.""" - # Ignore all arguments. - return ContinueProcessing.no - - -class Stop(End): - """The email 'stop' command (an alias for 'end').""" - - name = 'stop' diff --git a/mailman/commands/join.py b/mailman/commands/join.py deleted file mode 100644 index c14f3142b..000000000 --- a/mailman/commands/join.py +++ /dev/null @@ -1,126 +0,0 @@ -# Copyright (C) 2002-2009 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 . - -"""The email commands 'join' and 'subscribe'.""" - -__metaclass__ = type -__all__ = [ - 'Join', - 'Subscribe', - ] - - -from email.utils import formataddr, parseaddr -from zope.interface import implements - -from mailman.config import config -from mailman.i18n import _ -from mailman.interfaces.command import ContinueProcessing, IEmailCommand -from mailman.interfaces.member import DeliveryMode -from mailman.interfaces.registrar import IRegistrar - - - -class Join: - """The email 'join' command.""" - implements(IEmailCommand) - - name = 'join' - argument_description = '[digest=] [address=
      ]' - 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 -""") - - def process(self, mlist, msg, msgdata, arguments, results): - """See `IEmailCommand`.""" - # Parse the arguments. - address, delivery_mode = self._parse_arguments(arguments) - if address is None: - real_name, address = parseaddr(msg['from']) - # Address could be None or the empty string. - if not address: - address = msg.get_sender() - if not address: - print >> results, _( - '$self.name: No valid address found to subscribe') - return ContinueProcessing.no - domain = config.domains[mlist.host_name] - registrar = IRegistrar(domain) - registrar.register(address, real_name, mlist) - person = formataddr((real_name, address)) - print >> results, _('Confirmation email sent to $person') - return ContinueProcessing.yes - - def _parse_arguments(self, arguments): - """Parse command arguments. - - :param arguments: The sequences of arguments as given to the - `process()` method. - :return: address, delivery_mode - """ - address = None - delivery_mode = None - for argument in arguments: - parts = argument.split('=', 1) - if parts[0].lower() == 'digest': - if digest is not None: - print >> results, self.name, \ - _('duplicate argument: $argument') - return ContinueProcessing.no - if len(parts) == 0: - # We treat just plain 'digest' as 'digest=yes'. We don't - # yet support the other types of digest delivery. - delivery_mode = DeliveryMode.mime_digests - else: - if parts[1].lower() == 'yes': - delivery_mode = DeliveryMode.mime_digests - elif parts[1].lower() == 'no': - delivery_mode = DeliveryMode.regular - else: - print >> results, self.name, \ - _('bad argument: $argument') - return ContinueProcessing.no - elif parts[0].lower() == 'address': - if address is not None: - print >> results, self.name, \ - _('duplicate argument $argument') - return ContinueProcessing.no - if len(parts) == 0: - print >> results, self.name, \ - _('missing argument value: $argument') - return ContinueProcessing.no - if len(parts) > 1: - print >> results, self.name, \ - _('too many argument values: $argument') - return ContinueProcessing.no - address = parts[1] - return address, delivery_mode - - - -class Subscribe(Join): - """The email 'subscribe' command (an alias for 'join').""" - - name = 'subscribe' diff --git a/mailman/config/__init__.py b/mailman/config/__init__.py deleted file mode 100644 index 11f4f0c80..000000000 --- a/mailman/config/__init__.py +++ /dev/null @@ -1,30 +0,0 @@ -# Copyright (C) 2008-2009 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 . - -"""Mailman configuration package.""" - -from __future__ import absolute_import, unicode_literals - -__metaclass__ = type -__all__ = [ - 'config', - ] - - -from mailman.config.config import Configuration - -config = Configuration() diff --git a/mailman/config/config.py b/mailman/config/config.py deleted file mode 100644 index fa359a6f5..000000000 --- a/mailman/config/config.py +++ /dev/null @@ -1,206 +0,0 @@ -# Copyright (C) 2006-2009 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 . - -"""Configuration file loading and management.""" - -from __future__ import absolute_import, unicode_literals - -__metaclass__ = type -__all__ = [ - 'Configuration', - ] - - -import os -import sys -import errno -import logging - -from StringIO import StringIO -from lazr.config import ConfigSchema, as_boolean -from pkg_resources import resource_string - -from mailman import version -from mailman.core import errors -from mailman.domain import Domain -from mailman.languages import LanguageManager -from mailman.styles.manager import StyleManager -from mailman.utilities.filesystem import makedirs - - -SPACE = ' ' - - - -class Configuration(object): - """The core global configuration object.""" - - def __init__(self): - self.domains = {} # email host -> IDomain - self.switchboards = {} - self.languages = LanguageManager() - self.style_manager = StyleManager() - self.QFILE_SCHEMA_VERSION = version.QFILE_SCHEMA_VERSION - self._config = None - self.filename = None - # Create various registries. - self.chains = {} - self.rules = {} - self.handlers = {} - self.pipelines = {} - self.commands = {} - - def _clear(self): - """Clear the cached configuration variables.""" - self.domains.clear() - self.switchboards.clear() - self.languages = LanguageManager() - - def __getattr__(self, name): - """Delegate to the configuration object.""" - return getattr(self._config, name) - - def load(self, filename=None): - """Load the configuration from the schema and config files.""" - schema_string = resource_string('mailman.config', 'schema.cfg') - schema = ConfigSchema('schema.cfg', StringIO(schema_string)) - # If a configuration file was given, load it now too. First, load the - # absolute minimum default configuration, then if a configuration - # filename was given by the user, push it. - config_string = resource_string('mailman.config', 'mailman.cfg') - self._config = schema.loadFile(StringIO(config_string), 'mailman.cfg') - if filename is not None: - self.filename = filename - with open(filename) as user_config: - self._config.push(filename, user_config.read()) - self._post_process() - - def push(self, config_name, config_string): - """Push a new configuration onto the stack.""" - self._clear() - self._config.push(config_name, config_string) - self._post_process() - - def pop(self, config_name): - """Pop a configuration from the stack.""" - self._clear() - self._config.pop(config_name) - self._post_process() - - def _post_process(self): - """Perform post-processing after loading the configuration files.""" - # Set up the domains. - domains = self._config.getByCategory('domain', []) - for section in domains: - domain = Domain(section.email_host, section.base_url, - section.description, section.contact_address) - if domain.email_host in self.domains: - raise errors.BadDomainSpecificationError( - 'Duplicate email host: %s' % domain.email_host) - # Make sure there's only one mapping for the url_host - if domain.url_host in self.domains.values(): - raise errors.BadDomainSpecificationError( - 'Duplicate url host: %s' % domain.url_host) - # We'll do the reverse mappings on-demand. There shouldn't be too - # many virtual hosts that it will really matter that much. - self.domains[domain.email_host] = domain - # Set up directories. - self.BIN_DIR = os.path.abspath(os.path.dirname(sys.argv[0])) - self.VAR_DIR = var_dir = self._config.mailman.var_dir - # Now that we've loaded all the configuration files we're going to - # load, set up some useful directories. - join = os.path.join - self.LIST_DATA_DIR = join(var_dir, 'lists') - self.LOG_DIR = join(var_dir, 'logs') - self.LOCK_DIR = lockdir = join(var_dir, 'locks') - self.DATA_DIR = datadir = join(var_dir, 'data') - self.ETC_DIR = etcdir = join(var_dir, 'etc') - self.SPAM_DIR = join(var_dir, 'spam') - self.EXT_DIR = join(var_dir, 'ext') - self.QUEUE_DIR = join(var_dir, 'qfiles') - self.MESSAGES_DIR = join(var_dir, 'messages') - self.PUBLIC_ARCHIVE_FILE_DIR = join(var_dir, 'archives', 'public') - self.PRIVATE_ARCHIVE_FILE_DIR = join(var_dir, 'archives', 'private') - # Other useful files - self.PIDFILE = join(datadir, 'master-qrunner.pid') - self.SITE_PW_FILE = join(datadir, 'adm.pw') - self.LISTCREATOR_PW_FILE = join(datadir, 'creator.pw') - self.CONFIG_FILE = join(etcdir, 'mailman.cfg') - self.LOCK_FILE = join(lockdir, 'master-qrunner') - # Set up the switchboards. - from mailman.queue import Switchboard - Switchboard.initialize() - # Set up all the languages. - languages = self._config.getByCategory('language', []) - for language in languages: - code = language.name.split('.')[1] - self.languages.add_language(code, language.description, - language.charset, language.enabled) - # Always enable the server default language, which must be defined. - self.languages.enable_language(self._config.mailman.default_language) - self.ensure_directories_exist() - self.style_manager.populate() - - @property - def logger_configs(self): - """Return all log config sections.""" - return self._config.getByCategory('logging', []) - - @property - def paths(self): - """Return a substitution dictionary of all path variables.""" - return dict((k, self.__dict__[k]) - for k in self.__dict__ - if k.endswith('_DIR')) - - def ensure_directories_exist(self): - """Create all path directories if they do not exist.""" - for variable, directory in self.paths.items(): - makedirs(directory) - - @property - def qrunner_configs(self): - """Iterate over all the qrunner configuration sections.""" - for section in self._config.getByCategory('qrunner', []): - yield section - - @property - def archivers(self): - """Iterate over all the enabled archivers.""" - for section in self._config.getByCategory('archiver', []): - if not as_boolean(section.enable): - continue - class_path = section['class'] - module_name, class_name = class_path.rsplit('.', 1) - __import__(module_name) - yield getattr(sys.modules[module_name], class_name)() - - @property - def style_configs(self): - """Iterate over all the style configuration sections.""" - for section in self._config.getByCategory('style', []): - yield section - - @property - def header_matches(self): - """Iterate over all spam matching headers. - - Values are 3-tuples of (header, pattern, chain) - """ - matches = self._config.getByCategory('spam.headers', []) - for match in matches: - yield (matches.header, matches.pattern, matches.chain) diff --git a/mailman/config/mailman.cfg b/mailman/config/mailman.cfg deleted file mode 100644 index 2bf528bea..000000000 --- a/mailman/config/mailman.cfg +++ /dev/null @@ -1,69 +0,0 @@ -# Copyright (C) 2008-2009 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 . - -# This is the absolute bare minimum base configuration file. User supplied -# configurations are pushed onto this. - -[language.en] - -[qrunner.archive] -class: mailman.queue.archive.ArchiveRunner - -[qrunner.bad] -class: mailman.queue.fake.BadRunner -# The shunt runner is just a placeholder for its switchboard. -start: no - -[qrunner.bounces] -class: mailman.queue.bounce.BounceRunner - -[qrunner.command] -class: mailman.queue.command.CommandRunner - -[qrunner.in] -class: mailman.queue.incoming.IncomingRunner - -[qrunner.lmtp] -class: mailman.queue.lmtp.LMTPRunner - -[qrunner.maildir] -class: mailman.queue.maildir.MaildirRunner -# This is still experimental. -start: no - -[qrunner.news] -class: mailman.queue.news.NewsRunner - -[qrunner.out] -class: mailman.queue.outgoing.OutgoingRunner - -[qrunner.pipeline] -class: mailman.queue.pipeline.PipelineRunner - -[qrunner.retry] -class: mailman.queue.retry.RetryRunner -sleep_time: 15m - -[qrunner.shunt] -class: mailman.queue.fake.ShuntRunner -# The shunt runner is just a placeholder for its switchboard. -start: no - -[qrunner.virgin] -class: mailman.queue.virgin.VirginRunner - -[style.default] diff --git a/mailman/config/schema.cfg b/mailman/config/schema.cfg deleted file mode 100644 index df20a7370..000000000 --- a/mailman/config/schema.cfg +++ /dev/null @@ -1,589 +0,0 @@ -# Copyright (C) 2008-2009 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 . - -# This is the GNU Mailman configuration schema. It defines the default -# configuration options for the core system and plugins. It uses ini-style -# formats under the lazr.config regime to define all system configuration -# options. See for details. - -[mailman] -# This address is the "site owner" address. Certain messages which must be -# delivered to a human, but which can't be delivered to a list owner (e.g. a -# bounce from a list owner), will be sent to this address. It should point to -# a human. -site_owner: changeme@example.com - -# This address is used as the from address whenever a message comes from some -# entity to which there is no natural reply recipient. Set this to a real -# human or to /dev/null. It will be appended with the host name of the list -# involved. This address must not bounce and it must not point to a Mailman -# process. -noreply_address: noreply - -# Where all the runtime data will be kept. This directory must exist. -var_dir: /tmp/mailman - -# The default language for this server. -default_language: en - -# When allowing only members to post to a mailing list, how is the sender of -# the message determined? If this variable is set to Yes, then first the -# message's envelope sender is used, with a fallback to the sender if there is -# no envelope sender. Set this variable to No to always use the sender. -# -# The envelope sender is set by the SMTP delivery and is thus less easily -# spoofed than the sender, which is typically just taken from the From: header -# and thus easily spoofed by the end-user. However, sometimes the envelope -# sender isn't set correctly and this will manifest itself by postings being -# held for approval even if they appear to come from a list member. If you -# are having this problem, set this variable to No, but understand that some -# spoofed messages may get through. -use_envelope_sender: no - -# Membership tests for posting purposes are usually performed by looking at a -# set of headers, passing the test if any of their values match a member of -# the list. Headers are checked in the order given in this variable. The -# value From_ means to use the envelope sender. Field names are case -# insensitive. This is a space separate list of headers. -sender_headers: from from_ reply-to sender - -# Mail command processor will ignore mail command lines after designated max. -email_commands_max_lines: 10 - -# Default length of time a pending request is live before it is evicted from -# the pending database. -pending_request_life: 3d - - -[passwords] -# When Mailman generates them, this is the default length of member passwords. -member_password_length: 8 - -# Specify the type of passwords to use, when Mailman generates the passwords -# itself, as would be the case for membership requests where the user did not -# fill in a password, or during list creation, when auto-generation of admin -# passwords was selected. -# -# Set this value to 'yes' for classic Mailman user-friendly(er) passwords. -# These generate semi-pronounceable passwords which are easier to remember. -# Set this value to 'no' to use more cryptographically secure, but harder to -# remember, passwords -- if your operating system and Python version support -# the necessary feature (specifically that /dev/urandom be available). -user_friendly_passwords: yes - - -[qrunner.master] -# Define which process queue runners, and how many of them, to start. - -# The full import path to the class for this queue runner. -class: mailman.queue.runner.Runner - -# The directory path that this queue runner scans. -path: $VAR_DIR/qfiles/$name - -# The number of parallel queue runners. This must be a power of 2. -instances: 1 - -# Whether to start this queue runner or not. -start: yes - -# The maximum number of restarts for this queue runner. When the runner exits -# because of an error or other unexpected problem, it is automatically -# restarted, until the maximum number of restarts has been reached. -max_restarts: 10 - -# The sleep interval for the queue runner. It wakes up once every interval to -# process the files in its slice of the queue directory. -sleep_time: 1s - -[database] -# Use this to set the Storm database engine URL. You generally have one -# primary database connection for all of Mailman. List data and most rosters -# will store their data in this database, although external rosters may access -# other databases in their own way. This string supports standard -# 'configuration' substitutions. -url: sqlite:///$DATA_DIR/mailman.db -debug: no - -[logging.template] -# This defines various log settings. The options available are: -# -# - level -- Overrides the default level; this may be any of the -# standard Python logging levels, case insensitive. -# - format -- Overrides the default format string -# - datefmt -- Overrides the default date format string -# - path -- Overrides the default logger path. This may be a relative -# path name, in which case it is relative to Mailman's LOG_DIR, -# or it may be an absolute path name. You cannot change the -# handler class that will be used. -# - propagate -- Boolean specifying whether to propagate log message from this -# logger to the root "mailman" logger. You cannot override -# settings for the root logger. -# -# In this section, you can define defaults for all loggers, which will be -# prefixed by 'mailman.'. Use subsections to override settings for specific -# loggers. The names of the available loggers are: -# -# - archiver -- All archiver output -# - bounce -- All bounce processing logs go here -# - config -- Configuration issues -# - debug -- Only used for development -# - error -- All exceptions go to this log -# - fromusenet -- Information related to the Usenet to Mailman gateway -# - http -- Internal wsgi-based web interface -# - locks -- Lock state changes -# - mischief -- Various types of hostile activity -# - post -- Information about messages posted to mailing lists -# - qrunner -- qrunner start/stops -# - smtp -- Successful SMTP activity -# - smtp-failure -- Unsuccessful SMTP activity -# - subscribe -- Information about leaves/joins -# - vette -- Information related to admindb activity -format: %(asctime)s (%(process)d) %(message)s -datefmt: %b %d %H:%M:%S %Y -propagate: no -level: info -path: mailman - -[logging.root] - -[logging.archiver] - -[logging.bounce] -path: bounce - -[logging.config] - -[logging.debug] -path: debug -level: debug - -[logging.error] - -[logging.fromusenet] - -[logging.http] - -[logging.locks] - -[logging.mischief] - -[logging.qrunner] - -[logging.smtp] -path: smtp - -# The smtp logger defines additional options for handling the logging of each -# attempted delivery. These format strings specify what information is logged -# for every message, every successful delivery, every refused delivery and -# every recipient failure. To disable a status message, set the value to 'no' -# (without the quotes). -# -# These template strings accept the following set of substitution -# placeholders, if available. -# -# msgid -- the Message-ID of the message in question -# listname -- the fully-qualified list name -# sender -- the sender if available -# recip -- the recipient address if available, or the number of -# recipients being delivered to -# size -- the approximate size of the message in bytes -# seconds -- the number of seconds the operation took -# refused -- the number of refused recipients -# smtpcode -- the SMTP success or failure code -# smtpmsg -- the SMTP success or failure message - -every: $msgid smtp to $listname for $recip recips, completed in $time seconds -success: $msgid post to $listname from $sender, $size bytes -refused: $msgid post to $listname from $sender, $size bytes, $refused failures -failure: $msgid delivery to $recip failed with code $smtpcode, $smtpmsg - - -[logging.subscribe] - -[logging.vette] - - -[domain.master] -# Site-wide domain defaults. To configure an individual -# domain, add a [domain.example_com] section with the overrides. - -# This is the host name for the email interface. -email_host: example.com -# This is the base url for the domain's web interface. It must include the -# url scheme. -base_url: http://example.com -# The contact address for this domain. This is advertised as the human to -# contact when users have problems with the lists in this domain. -contact_address: postmaster@example.com -# A short description of this domain. -description: An example domain. - - -[language.master] -# Template for language definitions. The section name must be [language.xx] -# where xx is the 2-character ISO code for the language. - -# The English name for the language. -description: English (USA) -# And the default character set for the language. -charset: us-ascii -# Whether the language is enabled or not. -enabled: yes - - -[spam.headers.template] -# This section defines basic header matching actions. Each spam.header -# section names a header to match (case-insensitively), a pattern to match -# against the header's value, and the chain to jump to when the match -# succeeds. -# -# The header value should not include the trailing colon. -header: X-Spam -# The pattern is always matched with re.IGNORECASE. -pattern: xyz -# The chain to jump to if the pattern matches. Maybe be any existing chain -# such as 'discard', 'reject', 'hold', or 'accept'. -chain: hold - - -[mta] -# The class defining the interface to the incoming mail transport agent. -incoming: mailman.mta.postfix.LMTP - -# The class defining the interface to the outgoing mail transport agent. -outgoing: mailman.mta.smtp_direct.process - -# How to connect to the outgoing MTA. -smtp_host: localhost -smtp_port: 25 - -# Where the LMTP server listens for connections. -lmtp_host: localhost -lmtp_port: 8025 - -# Ceiling on the number of recipients that can be specified in a single SMTP -# transaction. Set to 0 to submit the entire recipient list in one -# transaction. -max_recipients: 500 - -# Ceiling on the number of SMTP sessions to perform on a single socket -# connection. Some MTAs have limits. Set this to 0 to do as many as we like -# (i.e. your MTA has no limits). Set this to some number great than 0 and -# Mailman will close the SMTP connection and re-open it after this number of -# consecutive sessions. -max_sessions_per_connection: 0 - -# Maximum number of simultaneous subthreads that will be used for SMTP -# delivery. After the recipients list is chunked according to max_recipients, -# each chunk is handed off to the SMTP server by a separate such thread. If -# your Python interpreter was not built for threads, this feature is disabled. -# You can explicitly disable it in all cases by setting max_delivery_threads -# to 0. -max_delivery_threads: 0 - -# How long should messages which have delivery failures continue to be -# retried? After this period of time, a message that has failed recipients -# will be dequeued and those recipients will never receive the message. -delivery_retry_period: 5d - -# These variables control the format and frequency of VERP-like delivery for -# better bounce detection. VERP is Variable Envelope Return Path, defined -# here: -# -# http://cr.yp.to/proto/verp.txt -# -# This involves encoding the address of the recipient as we (Mailman) know it -# into the envelope sender address (i.e. the SMTP `MAIL FROM:' address). -# Thus, no matter what kind of forwarding the recipient has in place, should -# it eventually bounce, we will receive an unambiguous notice of the bouncing -# address. -# -# However, we're technically only "VERP-like" because we're doing the envelope -# sender encoding in Mailman, not in the MTA. We do require cooperation from -# the MTA, so you must be sure your MTA can be configured for extended address -# semantics. -# -# The first variable describes how to encode VERP envelopes. It must contain -# these three string interpolations: -# -# $bounces -- the list-bounces mailbox will be set here -# $mailbox -- the recipient's mailbox will be set here -# $host -- the recipient's host name will be set here -# -# This example uses the default below. -# -# FQDN list address is: mylist@dom.ain -# Recipient is: aperson@a.nother.dom -# -# The envelope sender will be mylist-bounces+aperson=a.nother.dom@dom.ain -# -# Note that your MTA /must/ be configured to deliver such an addressed message -# to mylist-bounces! -verp_delimiter: + -verp_format: ${bounces}+${mailbox}=${host} - -# For nicer confirmation emails, use a VERP-like format which encodes the -# confirmation cookie in the reply address. This lets us put a more user -# friendly Subject: on the message, but requires cooperation from the MTA. -# Format is like verp_format, but with the following substitutions: -# -# $address -- the list-confirm address -# $cookie -- the confirmation cookie -verp_confirm_format: $address+$cookie - -# This is analogous to verp_regexp, but for splitting apart the -# verp_confirm_format. MUAs have been observed that mung -# -# From: local_part@host -# -# into -# -# To: "local_part" -# -# when replying, so we skip everything up to '<' if any. -verp_confirm_regexp: ^(.*<)?(?P[^+]+?)\+(?P[^@]+)@.*$ - -# Set this to 'yes' to enable VERP-like (more user friendly) confirmations. -verp_confirmations: no - -# Another good opportunity is when regular delivery is personalized. Here -# again, we're already incurring the performance hit for addressing each -# individual recipient. Set this to 'yes' to enable VERPs on all personalized -# regular deliveries (personalized digests aren't supported yet). -verp_personalized_deliveries: no - -# And finally, we can VERP normal, non-personalized deliveries. However, -# because it can be a significant performance hit, we allow you to decide how -# often to VERP regular deliveries. This is the interval, in number of -# messages, to do a VERP recipient address. The same variable controls both -# regular and digest deliveries. Set to 0 to disable occasional VERPs, set to -# 1 to VERP every delivery, or to some number > 1 for only occasional VERPs. -verp_delivery_interval: 0 - -# VERP format and regexp for probe messages. -verp_probe_format: %(bounces)s+%(token)s -verp_probe_regexp: ^(?P[^+]+?)\+(?P[^@]+)@.*$ -# Set this 'yes' to activate VERP probe for disabling by bounce. -verp_probes: no - -# This is the maximum number of automatic responses sent to an address because -# of -request messages or posting hold messages. This limit prevents response -# loops between Mailman and misconfigured remote email robots. Mailman -# already inhibits automatic replies to any message labeled with a header -# "Precendence: bulk|list|junk". This is a fallback safety valve so it should -# be set fairly high. Set to 0 for no limit (probably useful only for -# debugging). -max_autoresponses_per_day: 10 - -# Some list posts and mail to the -owner address may contain DomainKey or -# DomainKeys Identified Mail (DKIM) signature headers . -# Various list transformations to the message such as adding a list header or -# footer or scrubbing attachments or even reply-to munging can break these -# signatures. It is generally felt that these signatures have value, even if -# broken and even if the outgoing message is resigned. However, some sites -# may wish to remove these headers by setting this to 'yes'. -remove_dkim_headers: no - -# This variable describe the program to use for regenerating the transport map -# db file, from the associated plain text files. The file being updated will -# be appended to this string (with a separating space), so it must be -# appropriate for os.system(). -postfix_map_cmd: /usr/sbin/postmap - - -[bounces] -# How often should the bounce qrunner process queued detected bounces? -register_bounces_every: 15m - - -[archiver.master] -# To add new archivers, define a new section based on this one, overriding the -# following values. - -# The class implementing the IArchiver interface. -class: mailman.archiving.prototype.Prototype - -# Set this to 'yes' to enable the archiver. -enable: no - -# The base url for the archiver. This is used to to calculate links to -# individual messages in the archive. -base_url: http://archive.example.com/ - -# If the archiver works by getting a copy of the message, this is the address -# to send the copy to. -recipient: archive@archive.example.com - -# If the archiver works by calling a command on the local machine, this is the -# command to call. -command: /bin/echo - - -[archiver.mhonarc] -# This is the stock MHonArc archiver. -class: mailman.archiving.mhonarc.MHonArc - -base_url: http://$hostname/archives/$fqdn_listname - - -[archiver.mail_archive] -# This is the stock mail-archive.com archiver. -class: mailman.archiving.mailarchive.MailArchive - -[archiver.pipermail] -# This is the stock Pipermail archiver. -class: mailman.archiving.pipermail.Pipermail - -# This sets the default `clobber date' policy for the archiver. When a -# message is to be archived either by Pipermail or an external archiver, -# Mailman can modify the Date: header to be the date the message was received -# instead of the Date: in the original message. This is useful if you -# typically receive messages with outrageous dates. Set this to 0 to retain -# the date of the original message, or to 1 to always clobber the date. Set -# it to 2 to perform `smart overrides' on the date; when the date is outside -# allowable_sane_date_skew (either too early or too late), then the received -# date is substituted instead. -clobber_date_policy: 2 -allowable_sane_date_skew: 15d - -# Pipermail archives contain the raw email addresses of the posting authors. -# Some view this as a goldmine for spam harvesters. Set this to 'yes' to -# moderately obscure email addresses, but note that this breaks mailto: URLs -# in the archives too. -obscure_email_addresses: yes - -# When the archive is public, should Pipermail also make the raw Unix mbox -# file publically available? -public_mbox: no - - -[archiver.prototype] -# This is a prototypical sample archiver. -class: mailman.archiving.prototype.Prototype - - -[style.master] -# The style's priority, with 0 being the lowest priority. -priority: 0 - -# The class implementing the IStyle interface, which applies the style. -class: mailman.styles.default.DefaultStyle - - -[scrubber] -# A filter module that converts from multipart messages to "flat" messages -# (i.e. containing a single payload). This is required for Pipermail, and you -# may want to set it to 0 for external archivers. You can also replace it -# with your own module as long as it contains a process() function that takes -# a MailList object and a Message object. It should raise -# Errors.DiscardMessage if it wants to throw the message away. Otherwise it -# should modify the Message object as necessary. -archive_scrubber: mailman.pipeline.scrubber - -# This variable defines what happens to text/html subparts. They can be -# stripped completely, escaped, or filtered through an external program. The -# legal values are: -# 0 - Strip out text/html parts completely, leaving a notice of the removal in -# the message. If the outer part is text/html, the entire message is -# discarded. -# 1 - Remove any embedded text/html parts, leaving them as HTML-escaped -# attachments which can be separately viewed. Outer text/html parts are -# simply HTML-escaped. -# 2 - Leave it inline, but HTML-escape it -# 3 - Remove text/html as attachments but don't HTML-escape them. Note: this -# is very dangerous because it essentially means anybody can send an HTML -# email to your site containing evil JavaScript or web bugs, or other -# nasty things, and folks viewing your archives will be susceptible. You -# should only consider this option if you do heavy moderation of your list -# postings. -# -# Note: given the current archiving code, it is not possible to leave -# text/html parts inline and un-escaped. I wouldn't think it'd be a good idea -# to do anyway. -# -# The value can also be a string, in which case it is the name of a command to -# filter the HTML page through. The resulting output is left in an attachment -# or as the entirety of the message when the outer part is text/html. The -# format of the string must include a $filename substitution variable which -# will contain the name of the temporary file that the program should operate -# on. It should write the processed message to stdout. Set this to -# HTML_TO_PLAIN_TEXT_COMMAND to specify an HTML to plain text conversion -# program. -archive_html_sanitizer: 1 - -# Control parameter whether the scrubber should use the message attachment's -# filename as is indicated by the filename parameter or use 'attachement-xxx' -# instead. The default is set 'no' because the applications on PC and Mac -# begin to use longer non-ascii filenames. -use_attachment_filename: no - -# Use of attachment filename extension per se is may be dangerous because -# viruses fakes it. You can set this 'yes' if you filter the attachment by -# filename extension. -use_attachment_filename_extension: no - - -[digests] -# Headers which should be kept in both RFC 1153 (plain) and MIME digests. RFC -# 1153 also specifies these headers in this exact order, so order matters. -# These are space separated and case insensitive. -mime_digest_keep_headers: - Date From To Cc Subject Message-ID Keywords - In-Reply-To References Content-Type MIME-Version - Content-Transfer-Encoding Precedence Reply-To - Message - -plain_digest_keep_headers: - Message Date From - Subject To Cc - Message-ID Keywords - Content-Type - - -[nntp] -# Set these variables if you need to authenticate to your NNTP server for -# Usenet posting or reading. If no authentication is necessary, specify None -# for both variables. -username: -password: - -# Set this if you have an NNTP server you prefer gatewayed lists to use. -host: - -# This controls how headers must be cleansed in order to be accepted by your -# NNTP server. Some servers like INN reject messages containing prohibited -# headers, or duplicate headers. The NNTP server may reject the message for -# other reasons, but there's little that can be programmatically done about -# that. -# -# These headers (case ignored) are removed from the original message. This is -# a whitespace separate list of headers. -remove_headers: - nntp-posting-host nntp-posting-date x-trace - x-complaints-to xref date-received posted - posting-version relay-version received - -# These headers are left alone, unless there are duplicates in the original -# message. Any second and subsequent headers are rewritten to the second -# named header (case preserved). This is a list of header pairs, one pair per -# line. -rewrite_duplicate_headers: - To X-Original-To - CC X-Original-CC - Content-Transfer-Encoding X-Original-Content-Transfer-Encoding - MIME-Version X-MIME-Version diff --git a/mailman/constants.py b/mailman/constants.py deleted file mode 100644 index 39c4547f8..000000000 --- a/mailman/constants.py +++ /dev/null @@ -1,44 +0,0 @@ -# Copyright (C) 2006-2009 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 . - -"""Various constants and enumerations.""" - -from __future__ import unicode_literals - -__metaclass__ = type -__all__ = [ - 'SystemDefaultPreferences', - ] - - -from zope.interface import implements - -from mailman.interfaces.member import DeliveryMode, DeliveryStatus -from mailman.interfaces.preferences import IPreferences - - - -class SystemDefaultPreferences: - implements(IPreferences) - - acknowledge_posts = False - hide_address = True - preferred_language = 'en' - receive_list_copy = True - receive_own_postings = True - delivery_mode = DeliveryMode.regular - delivery_status = DeliveryStatus.enabled diff --git a/mailman/core/__init__.py b/mailman/core/__init__.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/mailman/core/chains.py b/mailman/core/chains.py deleted file mode 100644 index 40b8c779f..000000000 --- a/mailman/core/chains.py +++ /dev/null @@ -1,118 +0,0 @@ -# Copyright (C) 2007-2009 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 . - -"""Application support for chain processing.""" - -from __future__ import absolute_import, unicode_literals - -__metaclass__ = type -__all__ = [ - 'initialize', - 'process', - ] - - -from mailman.chains.accept import AcceptChain -from mailman.chains.builtin import BuiltInChain -from mailman.chains.discard import DiscardChain -from mailman.chains.headers import HeaderMatchChain -from mailman.chains.hold import HoldChain -from mailman.chains.reject import RejectChain -from mailman.config import config -from mailman.interfaces.chain import LinkAction - - - -def process(mlist, msg, msgdata, start_chain='built-in'): - """Process the message through a chain. - - :param mlist: the IMailingList for this message. - :param msg: The Message object. - :param msgdata: The message metadata dictionary. - :param start_chain: The name of the chain to start the processing with. - """ - # Set up some bookkeeping. - chain_stack = [] - msgdata['rule_hits'] = hits = [] - msgdata['rule_misses'] = misses = [] - # Find the starting chain and begin iterating through its links. - chain = config.chains[start_chain] - chain_iter = chain.get_links(mlist, msg, msgdata) - # Loop until we've reached the end of all processing chains. - while chain: - # Iterate over all links in the chain. Do this outside a for-loop so - # we can capture a chain's link iterator in mid-flight. This supports - # the 'detour' link action - try: - link = next(chain_iter) - except StopIteration: - # This chain is exhausted. Pop the last chain on the stack and - # continue iterating through it. If there's nothing left on the - # chain stack then we're completely finished processing. - if len(chain_stack) == 0: - return - chain, chain_iter = chain_stack.pop() - continue - # Process this link. - if link.rule.check(mlist, msg, msgdata): - if link.rule.record: - hits.append(link.rule.name) - # The rule matched so run its action. - if link.action is LinkAction.jump: - chain = link.chain - chain_iter = chain.get_links(mlist, msg, msgdata) - continue - elif link.action is LinkAction.detour: - # Push the current chain so that we can return to it when - # the next chain is finished. - chain_stack.append((chain, chain_iter)) - chain = link.chain - chain_iter = chain.get_links(mlist, msg, msgdata) - continue - elif link.action is LinkAction.stop: - # Stop all processing. - return - elif link.action is LinkAction.defer: - # Just process the next link in the chain. - pass - elif link.action is LinkAction.run: - link.function(mlist, msg, msgdata) - else: - raise AssertionError( - 'Bad link action: {0}'.format(link.action)) - else: - # The rule did not match; keep going. - if link.rule.record: - misses.append(link.rule.name) - - - -def initialize(): - """Set up chains, both built-in and from the database.""" - for chain_class in (DiscardChain, HoldChain, RejectChain, AcceptChain): - chain = chain_class() - assert chain.name not in config.chains, ( - 'Duplicate chain name: {0}'.format(chain.name)) - config.chains[chain.name] = chain - # Set up a couple of other default chains. - chain = BuiltInChain() - config.chains[chain.name] = chain - # Create and initialize the header matching chain. - chain = HeaderMatchChain() - config.chains[chain.name] = chain - # XXX Read chains from the database and initialize them. - pass diff --git a/mailman/core/errors.py b/mailman/core/errors.py deleted file mode 100644 index 39401127e..000000000 --- a/mailman/core/errors.py +++ /dev/null @@ -1,172 +0,0 @@ -# Copyright (C) 1998-2009 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 . - -"""Mailman errors.""" - -from __future__ import absolute_import, unicode_literals - -__metaclass__ = type -__all__ = [ - 'AlreadyReceivingDigests', - 'AlreadyReceivingRegularDeliveries', - 'BadDomainSpecificationError', - 'BadPasswordSchemeError', - 'CantDigestError', - 'DiscardMessage', - 'EmailAddressError', - 'HandlerError', - 'HoldMessage', - 'HostileSubscriptionError', - 'InvalidEmailAddress', - 'LostHeldMessage', - 'MailmanError', - 'MailmanException', - 'MemberError', - 'MembershipIsBanned', - 'MustDigestError', - 'NotAMemberError', - 'PasswordError', - 'RejectMessage', - 'SomeRecipientsFailed', - 'SubscriptionError', - ] - - - -# Base class for all exceptions raised in Mailman (XXX except legacy string -# exceptions). -class MailmanException(Exception): - pass - - - -# "New" style membership exceptions (new w/ MM2.1) -class MemberError(MailmanException): pass -class NotAMemberError(MemberError): pass -class AlreadyReceivingDigests(MemberError): pass -class AlreadyReceivingRegularDeliveries(MemberError): pass -class CantDigestError(MemberError): pass -class MustDigestError(MemberError): pass -class MembershipIsBanned(MemberError): pass - - - -# New style class based exceptions. All the above errors should eventually be -# converted. - -class MailmanError(MailmanException): - """Base class for all Mailman errors.""" - pass - -class BadDomainSpecificationError(MailmanError): - """The specification of a virtual domain is invalid or duplicated.""" - - - -# Exception hierarchy for bad email address errors that can be raised from -# Utils.ValidateEmail() -class EmailAddressError(MailmanError): - """Base class for email address validation errors.""" - - -class InvalidEmailAddress(EmailAddressError): - """Email address is invalid.""" - - - -# Exceptions for admin request database -class LostHeldMessage(MailmanError): - """Held message was lost.""" - pass - - - -def _(s): - return s - -# Exceptions for the Handler subsystem -class HandlerError(MailmanError): - """Base class for all handler errors.""" - -class HoldMessage(HandlerError): - """Base class for all message-being-held short circuits.""" - - # funky spelling is necessary to break import loops - reason = _('For some unknown reason') - - def reason_notice(self): - return self.reason - - # funky spelling is necessary to break import loops - rejection = _('Your message was rejected') - - def rejection_notice(self, mlist): - return self.rejection - -class DiscardMessage(HandlerError): - """The message can be discarded with no further action""" - -class SomeRecipientsFailed(HandlerError): - """Delivery to some or all recipients failed""" - def __init__(self, tempfailures, permfailures): - HandlerError.__init__(self) - self.tempfailures = tempfailures - self.permfailures = permfailures - -class RejectMessage(HandlerError): - """The message will be bounced back to the sender""" - def __init__(self, notice=None): - super(RejectMessage, self).__init__() - if notice is None: - notice = _('Your message was rejected') - if notice.endswith('\n\n'): - pass - elif notice.endswith('\n'): - notice += '\n' - else: - notice += '\n\n' - self.notice = notice - - - -# Subscription exceptions -class SubscriptionError(MailmanError): - """Subscription errors base class.""" - - -class HostileSubscriptionError(SubscriptionError): - """A cross-subscription attempt was made. - - This exception gets raised when an invitee attempts to use the - invitation to cross-subscribe to some other mailing list. - """ - - - -class PasswordError(MailmanError): - """A password related error.""" - - -class BadPasswordSchemeError(PasswordError): - """A bad password scheme was given.""" - - def __init__(self, scheme_name='unknown'): - super(BadPasswordSchemeError, self).__init__() - self.scheme_name = scheme_name - - def __str__(self): - return 'A bad password scheme was given: %s' % self.scheme_name diff --git a/mailman/core/initialize.py b/mailman/core/initialize.py deleted file mode 100644 index bb16f0036..000000000 --- a/mailman/core/initialize.py +++ /dev/null @@ -1,125 +0,0 @@ -# Copyright (C) 2006-2009 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 . - -"""Initialize all global state. - -Every entrance into the Mailman system, be it by command line, mail program, -or cgi, must call the initialize function here in order for the system's -global state to be set up properly. Typically this is called after command -line argument parsing, since some of the initialization behavior is controlled -by the command line arguments. -""" - -from __future__ import absolute_import, unicode_literals - -__metaclass__ = type -__all__ = [ - 'initialize', - 'initialize_1', - 'initialize_2', - 'initialize_3', - ] - - -import os - -from zope.interface.interface import adapter_hooks -from zope.interface.verify import verifyObject - -import mailman.config.config -import mailman.core.logging - -from mailman.core.plugins import get_plugin -from mailman.interfaces.database import IDatabase - - - -# These initialization calls are separated for the testing framework, which -# needs to do some internal calculations after config file loading and log -# initialization, but before database initialization. Generally all other -# code will just call initialize(). - -def initialize_1(config_path=None, propagate_logs=None): - """First initialization step. - - * The configuration system - * Run-time directories - * The logging subsystem - - :param config_path: The path to the configuration file. - :type config_path: string - :param propagate_logs: Should the log output propagate to stderr? - :type propagate_logs: boolean or None - """ - # By default, set the umask so that only owner and group can read and - # write our files. Specifically we must have g+rw and we probably want - # o-rwx although I think in most cases it doesn't hurt if other can read - # or write the files. Note that the Pipermail archive has more - # restrictive permissions in order to handle private archives, but it - # handles that correctly. - os.umask(007) - mailman.config.config.load(config_path) - # Create the queue and log directories if they don't already exist. - mailman.config.config.ensure_directories_exist() - mailman.core.logging.initialize(propagate_logs) - - -def initialize_2(debug=False): - """Second initialization step. - - * Rules - * Chains - * Pipelines - * Commands - - :param debug: Should the database layer be put in debug mode? - :type debug: boolean - """ - database_plugin = get_plugin('mailman.database') - # Instantiate the database plugin, ensure that it's of the right type, and - # initialize it. Then stash the object on our configuration object. - database = database_plugin() - verifyObject(IDatabase, database) - database.initialize(debug) - mailman.config.config.db = database - # Initialize the rules and chains. Do the imports here so as to avoid - # circular imports. - from mailman.app.commands import initialize as initialize_commands - from mailman.core.chains import initialize as initialize_chains - from mailman.core.pipelines import initialize as initialize_pipelines - from mailman.core.rules import initialize as initialize_rules - # Order here is somewhat important. - initialize_rules() - initialize_chains() - initialize_pipelines() - initialize_commands() - - -def initialize_3(): - """Third initialization step. - - * Adapters - """ - from mailman.app.registrar import adapt_domain_to_registrar - adapter_hooks.append(adapt_domain_to_registrar) - - - -def initialize(config_path=None, propagate_logs=None): - initialize_1(config_path, propagate_logs) - initialize_2() - initialize_3() diff --git a/mailman/core/logging.py b/mailman/core/logging.py deleted file mode 100644 index a18065965..000000000 --- a/mailman/core/logging.py +++ /dev/null @@ -1,163 +0,0 @@ -# Copyright (C) 2006-2009 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 . - -"""Logging initialization, using Python's standard logging package.""" - -from __future__ import absolute_import, unicode_literals - -__metaclass__ = type -__all__ = [ - 'initialize', - 'reopen', - ] - - -import os -import sys -import codecs -import logging - -from lazr.config import as_boolean, as_log_level - -from mailman.config import config - - -_handlers = {} - - - -# XXX I would love to simplify things and use Python 2.6's WatchedFileHandler, -# but there are two problems. First, it's more difficult to handle the test -# suite's need to reopen the file handler to a different path. Does -# zope.testing's logger support fix this? -# -# The other problem is that WatchedFileHandler doesn't really easily support -# HUPing the process to reopen the log file. Now, maybe that's not a big deal -# because the standard logging module would already handle things correctly if -# the file is moved, but still that's not an interface I'm ready to give up on -# yet. For now, keep our hack. - -class ReopenableFileHandler(logging.Handler): - """A file handler that supports reopening.""" - - def __init__(self, name, filename): - self.name = name - self._filename = filename - self._stream = self._open() - logging.Handler.__init__(self) - - def _open(self): - return codecs.open(self._filename, 'a', 'utf-8') - - def flush(self): - if self._stream: - self._stream.flush() - - def emit(self, record): - # It's possible for the stream to have been closed by the time we get - # here, due to the shut down semantics. This mostly happens in the - # test suite, but be defensive anyway. - stream = (self._stream if self._stream else sys.stderr) - try: - msg = self.format(record) - try: - stream.write('{0}'.format(msg)) - except UnicodeError: - stream.write('{0}'.format(msg.encode('string-escape'))) - self.flush() - except: - self.handleError(record) - - def close(self): - self.flush() - self._stream.close() - self._stream = None - logging.Handler.close(self) - - def reopen(self, filename=None): - """Reopen the output stream. - - :param filename: If given, this reopens the output stream to a new - file. This is used in the test suite. - :type filename: string - """ - if filename is not None: - self._filename = filename - self._stream.close() - self._stream = self._open() - - - -def initialize(propagate=None): - """Initialize all logs. - - :param propagate: Flag specifying whether logs should propagate their - messages to the root logger. If omitted, propagation is determined - from the configuration files. - :type propagate: bool or None - """ - # First, find the root logger and configure the logging subsystem. - # Initialize the root logger, then create a formatter for all the - # sublogs. The root logger should log to stderr. - logging.basicConfig(format=config.logging.root.format, - datefmt=config.logging.root.datefmt, - level=as_log_level(config.logging.root.level), - stream=sys.stderr) - # Create the subloggers. - for logger_config in config.logger_configs: - sub_name = logger_config.name.split('.')[-1] - if sub_name == 'root': - continue - logger_name = 'mailman.' + sub_name - log = logging.getLogger(logger_name) - # Get settings from log configuration file (or defaults). - log_format = logger_config.format - log_datefmt = logger_config.datefmt - # Propagation to the root logger is how we handle logging to stderr - # when the qrunners are not run as a subprocess of mailmanctl. - log.propagate = (as_boolean(logger_config.propagate) - if propagate is None else propagate) - # Set the logger's level. - log.setLevel(as_log_level(logger_config.level)) - # Create a formatter for this logger, then a handler, and link the - # formatter to the handler. - formatter = logging.Formatter(fmt=log_format, datefmt=log_datefmt) - path_str = logger_config.path - path_abs = os.path.normpath(os.path.join(config.LOG_DIR, path_str)) - handler = ReopenableFileHandler(sub_name, path_abs) - _handlers[sub_name] = handler - handler.setFormatter(formatter) - log.addHandler(handler) - - - -def reopen(): - """Re-open all log files.""" - for handler in _handlers.values(): - handler.reopen() - - - -def get_handler(sub_name): - """Return the handler associated with a named logger. - - :param sub_name: The logger name, sans the 'mailman.' prefix. - :type sub_name: string - :return: The file handler associated with the named logger. - :rtype: `ReopenableFileHandler` - """ - return _handlers[sub_name] diff --git a/mailman/core/pipelines.py b/mailman/core/pipelines.py deleted file mode 100644 index 8aae5cc25..000000000 --- a/mailman/core/pipelines.py +++ /dev/null @@ -1,125 +0,0 @@ -# Copyright (C) 2008-2009 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 . - -"""Pipeline processor.""" - -from __future__ import absolute_import, unicode_literals - -__metaclass__ = type -__all__ = [ - 'initialize', - 'process', - ] - - -from zope.interface import implements -from zope.interface.verify import verifyObject - -from mailman.config import config -from mailman.core.plugins import get_plugins -from mailman.i18n import _ -from mailman.interfaces.handler import IHandler -from mailman.interfaces.pipeline import IPipeline - - - -def process(mlist, msg, msgdata, pipeline_name='built-in'): - """Process the message through the given pipeline. - - :param mlist: the IMailingList for this message. - :param msg: The Message object. - :param msgdata: The message metadata dictionary. - :param pipeline_name: The name of the pipeline to process through. - """ - pipeline = config.pipelines[pipeline_name] - for handler in pipeline: - handler.process(mlist, msg, msgdata) - - - -class BasePipeline: - """Base pipeline implementation.""" - - implements(IPipeline) - - _default_handlers = () - - def __init__(self): - self._handlers = [] - for handler_name in self._default_handlers: - self._handlers.append(config.handlers[handler_name]) - - def __iter__(self): - """See `IPipeline`.""" - for handler in self._handlers: - yield handler - - -class BuiltInPipeline(BasePipeline): - """The built-in pipeline.""" - - name = 'built-in' - description = _('The built-in pipeline.') - - _default_handlers = ( - 'mime-delete', - 'scrubber', - 'tagger', - 'calculate-recipients', - 'avoid-duplicates', - 'cleanse', - 'cleanse-dkim', - 'cook-headers', - 'to-digest', - 'to-archive', - 'to-usenet', - 'after-delivery', - 'acknowledge', - 'to-outgoing', - ) - - -class VirginPipeline(BasePipeline): - """The processing pipeline for virgin messages. - - Virgin messages are those that are crafted internally by Mailman. - """ - name = 'virgin' - description = _('The virgin queue pipeline.') - - _default_handlers = ( - 'cook-headers', - 'to-outgoing', - ) - - - -def initialize(): - """Initialize the pipelines.""" - # Find all handlers in the registered plugins. - for handler_finder in get_plugins('mailman.handlers'): - for handler_class in handler_finder(): - handler = handler_class() - verifyObject(IHandler, handler) - assert handler.name not in config.handlers, ( - 'Duplicate handler "{0}" found in {1}'.format( - handler.name, handler_finder)) - config.handlers[handler.name] = handler - # Set up some pipelines. - for pipeline_class in (BuiltInPipeline, VirginPipeline): - pipeline = pipeline_class() - config.pipelines[pipeline.name] = pipeline diff --git a/mailman/core/plugins.py b/mailman/core/plugins.py deleted file mode 100644 index e9ba26571..000000000 --- a/mailman/core/plugins.py +++ /dev/null @@ -1,74 +0,0 @@ -# Copyright (C) 2007-2009 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 . - -"""Get a requested plugin.""" - -from __future__ import absolute_import, unicode_literals - -__metaclass__ = type -__all__ = [ - ] - - -import pkg_resources - - - -def get_plugin(group): - """Get the named plugin. - - In general, this returns exactly one plugin. If no plugins have been - added to the named group, the 'stock' plugin will be used. If more than - one plugin -- other than the stock one -- exists, an exception will be - raised. - - :param group: The plugin group name. - :return: The loaded plugin. - :raises RuntimeError: If more than one plugin overrides the stock plugin - for the named group. - """ - entry_points = list(pkg_resources.iter_entry_points(group)) - if len(entry_points) == 0: - raise RuntimeError( - 'No entry points found for group: {0}'.format(group)) - elif len(entry_points) == 1: - # Okay, this is the one to use. - return entry_points[0].load() - elif len(entry_points) == 2: - # Find the one /not/ named 'stock'. - entry_points = [ep for ep in entry_points if ep.name <> 'stock'] - if len(entry_points) == 0: - raise RuntimeError( - 'No stock plugin found for group: {0}'.format(group)) - elif len(entry_points) == 2: - raise RuntimeError('Too many stock plugins defined') - else: - raise AssertionError('Insanity') - return entry_points[0].load() - else: - raise RuntimeError('Too many plugins for group: {0}'.format(group)) - - - -def get_plugins(group): - """Get and return all plugins in the named group. - - :param group: Plugin group name. - :return: The loaded plugin. - """ - for entry_point in pkg_resources.iter_entry_points(group): - yield entry_point.load() diff --git a/mailman/core/rules.py b/mailman/core/rules.py deleted file mode 100644 index 83e24dfa2..000000000 --- a/mailman/core/rules.py +++ /dev/null @@ -1,46 +0,0 @@ -# Copyright (C) 2007-2009 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 . - -"""Various rule helpers""" - -from __future__ import absolute_import, unicode_literals - -__metaclass__ = type -__all__ = [ - 'initialize', - ] - - -from zope.interface.verify import verifyObject - -from mailman.config import config -from mailman.core.plugins import get_plugins -from mailman.interfaces.rules import IRule - - - -def initialize(): - """Find and register all rules in all plugins.""" - # Find rules in plugins. - for rule_finder in get_plugins('mailman.rules'): - for rule_class in rule_finder(): - rule = rule_class() - verifyObject(IRule, rule) - assert rule.name not in config.rules, ( - 'Duplicate rule "{0}" found in {1}'.format( - rule.name, rule_finder)) - config.rules[rule.name] = rule diff --git a/mailman/database/__init__.py b/mailman/database/__init__.py deleted file mode 100644 index 8b7f584c2..000000000 --- a/mailman/database/__init__.py +++ /dev/null @@ -1,153 +0,0 @@ -# Copyright (C) 2006-2009 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 . - -from __future__ import absolute_import, unicode_literals - -__metaclass__ = type -__all__ = [ - 'StockDatabase', - ] - -import os -import logging - -from locknix.lockfile import Lock -from lazr.config import as_boolean -from pkg_resources import resource_string -from storm.locals import create_database, Store -from urlparse import urlparse -from zope.interface import implements - -import mailman.version - -from mailman.config import config -from mailman.database.listmanager import ListManager -from mailman.database.messagestore import MessageStore -from mailman.database.pending import Pendings -from mailman.database.requests import Requests -from mailman.database.usermanager import UserManager -from mailman.database.version import Version -from mailman.interfaces.database import IDatabase, SchemaVersionMismatchError -from mailman.utilities.string import expand - -log = logging.getLogger('mailman.config') - - - -class StockDatabase: - """The standard database, using Storm on top of SQLite.""" - - implements(IDatabase) - - def __init__(self): - self.list_manager = None - self.user_manager = None - self.message_store = None - self.pendings = None - self.requests = None - self._store = None - - def initialize(self, debug=None): - """See `IDatabase`.""" - # Serialize this so we don't get multiple processes trying to create - # the database at the same time. - with Lock(os.path.join(config.LOCK_DIR, 'dbcreate.lck')): - self._create(debug) - self.list_manager = ListManager() - self.user_manager = UserManager() - self.message_store = MessageStore() - self.pendings = Pendings() - self.requests = Requests() - - def begin(self): - """See `IDatabase`.""" - # Storm takes care of this for us. - pass - - def commit(self): - """See `IDatabase`.""" - self.store.commit() - - def abort(self): - """See `IDatabase`.""" - self.store.rollback() - - def _create(self, debug): - # Calculate the engine url. - url = expand(config.database.url, config.paths) - log.debug('Database url: %s', url) - # XXX By design of SQLite, database file creation does not honor - # umask. See their ticket #1193: - # http://www.sqlite.org/cvstrac/tktview?tn=1193,31 - # - # This sucks for us because the mailman.db file /must/ be group - # writable, however even though we guarantee our umask is 002 here, it - # still gets created without the necessary g+w permission, due to - # SQLite's policy. This should only affect SQLite engines because its - # the only one that creates a little file on the local file system. - # This kludges around their bug by "touch"ing the database file before - # SQLite has any chance to create it, thus honoring the umask and - # ensuring the right permissions. We only try to do this for SQLite - # engines, and yes, we could have chmod'd the file after the fact, but - # half dozen and all... - touch(url) - database = create_database(url) - store = Store(database) - database.DEBUG = (as_boolean(config.database.debug) - if debug is None else debug) - # Check the sqlite master database to see if the version file exists. - # If so, then we assume the database schema is correctly initialized. - # Storm does not currently have schema creation. This is not an ideal - # way to handle creating the database, but it's cheap and easy for - # now. - table_names = [item[0] for item in - store.execute('select tbl_name from sqlite_master;')] - if 'version' not in table_names: - # Initialize the database. - sql = resource_string('mailman.database', 'mailman.sql') - for statement in sql.split(';'): - store.execute(statement + ';') - # Validate schema version. - v = store.find(Version, component=u'schema').one() - if not v: - # Database has not yet been initialized - v = Version(component='schema', - version=mailman.version.DATABASE_SCHEMA_VERSION) - store.add(v) - elif v.version <> mailman.version.DATABASE_SCHEMA_VERSION: - # XXX Update schema - raise SchemaVersionMismatchError(v.version) - self.store = store - store.commit() - - def _reset(self): - """See `IDatabase`.""" - from mailman.database.model import ModelMeta - self.store.rollback() - ModelMeta._reset(self.store) - - - -def touch(url): - parts = urlparse(url) - if parts.scheme <> 'sqlite': - return - path = os.path.normpath(parts.path) - fd = os.open(path, os.O_WRONLY | os.O_NONBLOCK | os.O_CREAT, 0666) - # Ignore errors - if fd > 0: - os.close(fd) diff --git a/mailman/database/address.py b/mailman/database/address.py deleted file mode 100644 index 528d3af51..000000000 --- a/mailman/database/address.py +++ /dev/null @@ -1,96 +0,0 @@ -# Copyright (C) 2006-2009 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 . - -"""Model for addresses.""" - -from __future__ import absolute_import, unicode_literals - -__metaclass__ = type -__all__ = [ - 'Address', - ] - - -from email.utils import formataddr -from storm.locals import * -from zope.interface import implements - -from mailman.config import config -from mailman.database.member import Member -from mailman.database.model import Model -from mailman.database.preferences import Preferences -from mailman.interfaces.member import AlreadySubscribedError -from mailman.interfaces.address import IAddress - - - -class Address(Model): - implements(IAddress) - - id = Int(primary=True) - address = Unicode() - _original = Unicode() - real_name = Unicode() - verified_on = DateTime() - registered_on = DateTime() - - user_id = Int() - user = Reference(user_id, 'User.id') - preferences_id = Int() - preferences = Reference(preferences_id, 'Preferences.id') - - def __init__(self, address, real_name): - super(Address, self).__init__() - lower_case = address.lower() - self.address = lower_case - self.real_name = real_name - self._original = (None if lower_case == address else address) - - def __str__(self): - addr = (self.address if self._original is None else self._original) - return formataddr((self.real_name, addr)) - - def __repr__(self): - verified = ('verified' if self.verified_on else 'not verified') - address_str = str(self) - if self._original is None: - return ''.format( - address_str, verified, id(self)) - else: - return ''.format( - address_str, verified, self.address, id(self)) - - def subscribe(self, mailing_list, role): - # This member has no preferences by default. - member = config.db.store.find( - Member, - Member.role == role, - Member.mailing_list == mailing_list.fqdn_listname, - Member.address == self).one() - if member: - raise AlreadySubscribedError( - mailing_list.fqdn_listname, self.address, role) - member = Member(role=role, - mailing_list=mailing_list.fqdn_listname, - address=self) - member.preferences = Preferences() - config.db.store.add(member) - return member - - @property - def original_address(self): - return (self.address if self._original is None else self._original) diff --git a/mailman/database/language.py b/mailman/database/language.py deleted file mode 100644 index 8adc5c4a5..000000000 --- a/mailman/database/language.py +++ /dev/null @@ -1,40 +0,0 @@ -# Copyright (C) 2006-2009 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 . - -"""Model for languages.""" - -from __future__ import absolute_import, unicode_literals - -__metaclass__ = type -__all__ = [ - 'Language', - ] - - -from storm.locals import * -from zope.interface import implements - -from mailman.database import Model -from mailman.interfaces import ILanguage - - - -class Language(Model): - implements(ILanguage) - - id = Int(primary=True) - code = Unicode() diff --git a/mailman/database/listmanager.py b/mailman/database/listmanager.py deleted file mode 100644 index 790a2509a..000000000 --- a/mailman/database/listmanager.py +++ /dev/null @@ -1,82 +0,0 @@ -# Copyright (C) 2007-2009 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 . - -"""A mailing list manager.""" - -from __future__ import absolute_import, unicode_literals - -__metaclass__ = type -__all__ = [ - 'ListManager', - ] - - -import datetime - -from zope.interface import implements - -from mailman.config import config -from mailman.database.mailinglist import MailingList -from mailman.interfaces.listmanager import IListManager, ListAlreadyExistsError - - - -class ListManager(object): - """An implementation of the `IListManager` interface.""" - - implements(IListManager) - - def create(self, fqdn_listname): - """See `IListManager`.""" - listname, hostname = fqdn_listname.split('@', 1) - mlist = config.db.store.find( - MailingList, - MailingList.list_name == listname, - MailingList.host_name == hostname).one() - if mlist: - raise ListAlreadyExistsError(fqdn_listname) - mlist = MailingList(fqdn_listname) - mlist.created_at = datetime.datetime.now() - config.db.store.add(mlist) - return mlist - - def get(self, fqdn_listname): - """See `IListManager`.""" - listname, hostname = fqdn_listname.split('@', 1) - mlist = config.db.store.find(MailingList, - list_name=listname, - host_name=hostname).one() - if mlist is not None: - # XXX Fixme - mlist._restore() - return mlist - - def delete(self, mlist): - """See `IListManager`.""" - config.db.store.remove(mlist) - - @property - def mailing_lists(self): - """See `IListManager`.""" - for fqdn_listname in self.names: - yield self.get(fqdn_listname) - - @property - def names(self): - """See `IListManager`.""" - for mlist in config.db.store.find(MailingList): - yield '{0}@{1}'.format(mlist.list_name, mlist.host_name) diff --git a/mailman/database/mailinglist.py b/mailman/database/mailinglist.py deleted file mode 100644 index 8803a5fa4..000000000 --- a/mailman/database/mailinglist.py +++ /dev/null @@ -1,272 +0,0 @@ -# Copyright (C) 2006-2009 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 . - -"""Model for mailing lists.""" - -from __future__ import absolute_import, unicode_literals - -__metaclass__ = type -__all__ = [ - 'MailingList', - ] - - -import os -import string - -from storm.locals import * -from urlparse import urljoin -from zope.interface import implements - -from mailman.config import config -from mailman.database import roster -from mailman.database.model import Model -from mailman.database.types import Enum -from mailman.interfaces.mailinglist import IMailingList, Personalization -from mailman.utilities.filesystem import makedirs -from mailman.utilities.string import expand - - -SPACE = ' ' -UNDERSCORE = '_' - - - -class MailingList(Model): - implements(IMailingList) - - id = Int(primary=True) - - # List identity - list_name = Unicode() - host_name = Unicode() - # Attributes not directly modifiable via the web u/i - created_at = DateTime() - admin_member_chunksize = Int() - hold_and_cmd_autoresponses = Pickle() - # Attributes which are directly modifiable via the web u/i. The more - # complicated attributes are currently stored as pickles, though that - # will change as the schema and implementation is developed. - next_request_id = Int() - next_digest_number = Int() - admin_responses = Pickle() - postings_responses = Pickle() - request_responses = Pickle() - digest_last_sent_at = Float() - one_last_digest = Pickle() - volume = Int() - last_post_time = DateTime() - # Attributes which are directly modifiable via the web u/i. The more - # complicated attributes are currently stored as pickles, though that - # will change as the schema and implementation is developed. - accept_these_nonmembers = Pickle() - acceptable_aliases = Pickle() - admin_immed_notify = Bool() - admin_notify_mchanges = Bool() - administrivia = Bool() - advertised = Bool() - anonymous_list = Bool() - archive = Bool() - archive_private = Bool() - archive_volume_frequency = Int() - autorespond_admin = Bool() - autorespond_postings = Bool() - autorespond_requests = Int() - autoresponse_admin_text = Unicode() - autoresponse_graceperiod = TimeDelta() - autoresponse_postings_text = Unicode() - autoresponse_request_text = Unicode() - ban_list = Pickle() - bounce_info_stale_after = TimeDelta() - bounce_matching_headers = Unicode() - bounce_notify_owner_on_disable = Bool() - bounce_notify_owner_on_removal = Bool() - bounce_processing = Bool() - bounce_score_threshold = Int() - bounce_unrecognized_goes_to_list_owner = Bool() - bounce_you_are_disabled_warnings = Int() - bounce_you_are_disabled_warnings_interval = TimeDelta() - collapse_alternatives = Bool() - convert_html_to_plaintext = Bool() - default_member_moderation = Bool() - description = Unicode() - digest_footer = Unicode() - digest_header = Unicode() - digest_is_default = Bool() - digest_send_periodic = Bool() - digest_size_threshold = Int() - digest_volume_frequency = Int() - digestable = Bool() - discard_these_nonmembers = Pickle() - emergency = Bool() - encode_ascii_prefixes = Bool() - filter_action = Int() - filter_content = Bool() - filter_filename_extensions = Pickle() - filter_mime_types = Pickle() - first_strip_reply_to = Bool() - forward_auto_discards = Bool() - gateway_to_mail = Bool() - gateway_to_news = Bool() - generic_nonmember_action = Int() - goodbye_msg = Unicode() - header_matches = Pickle() - hold_these_nonmembers = Pickle() - include_list_post_header = Bool() - include_rfc2369_headers = Bool() - info = Unicode() - linked_newsgroup = Unicode() - max_days_to_hold = Int() - max_message_size = Int() - max_num_recipients = Int() - member_moderation_action = Enum() - member_moderation_notice = Unicode() - mime_is_default_digest = Bool() - moderator_password = Unicode() - msg_footer = Unicode() - msg_header = Unicode() - new_member_options = Int() - news_moderation = Enum() - news_prefix_subject_too = Bool() - nntp_host = Unicode() - nondigestable = Bool() - nonmember_rejection_notice = Unicode() - obscure_addresses = Bool() - pass_filename_extensions = Pickle() - pass_mime_types = Pickle() - personalize = Enum() - pipeline = Unicode() - post_id = Int() - preferred_language = Unicode() - private_roster = Bool() - real_name = Unicode() - reject_these_nonmembers = Pickle() - reply_goes_to_list = Enum() - reply_to_address = Unicode() - require_explicit_destination = Bool() - respond_to_post_requests = Bool() - scrub_nondigest = Bool() - send_goodbye_msg = Bool() - send_reminders = Bool() - send_welcome_msg = Bool() - start_chain = Unicode() - subject_prefix = Unicode() - subscribe_auto_approval = Pickle() - subscribe_policy = Int() - topics = Pickle() - topics_bodylines_limit = Int() - topics_enabled = Bool() - unsubscribe_policy = Int() - welcome_msg = Unicode() - - def __init__(self, fqdn_listname): - super(MailingList, self).__init__() - listname, hostname = fqdn_listname.split('@', 1) - self.list_name = listname - self.host_name = hostname - # For the pending database - self.next_request_id = 1 - self._restore() - # Max autoresponses per day. A mapping between addresses and a - # 2-tuple of the date of the last autoresponse and the number of - # autoresponses sent on that date. - self.hold_and_cmd_autoresponses = {} - self.personalization = Personalization.none - self.real_name = string.capwords( - SPACE.join(listname.split(UNDERSCORE))) - makedirs(self.data_path) - - # XXX FIXME - def _restore(self): - self.owners = roster.OwnerRoster(self) - self.moderators = roster.ModeratorRoster(self) - self.administrators = roster.AdministratorRoster(self) - self.members = roster.MemberRoster(self) - self.regular_members = roster.RegularMemberRoster(self) - self.digest_members = roster.DigestMemberRoster(self) - self.subscribers = roster.Subscribers(self) - - @property - def fqdn_listname(self): - """See `IMailingList`.""" - return '{0}@{1}'.format(self.list_name, self.host_name) - - @property - def web_host(self): - """See `IMailingList`.""" - return config.domains[self.host_name] - - def script_url(self, target, context=None): - """See `IMailingList`.""" - # Find the domain for this mailing list. - domain = config.domains[self.host_name] - # XXX Handle the case for when context is not None; those would be - # relative URLs. - return urljoin(domain.base_url, target + '/' + self.fqdn_listname) - - @property - def data_path(self): - """See `IMailingList`.""" - return os.path.join(config.LIST_DATA_DIR, self.fqdn_listname) - - # IMailingListAddresses - - @property - def posting_address(self): - return self.fqdn_listname - - @property - def no_reply_address(self): - return '{0}@{1}'.format(config.mailman.noreply_address, self.host_name) - - @property - def owner_address(self): - return '{0}-owner@{1}'.format(self.list_name, self.host_name) - - @property - def request_address(self): - return '{0}-request@{1}'.format(self.list_name, self.host_name) - - @property - def bounces_address(self): - return '{0}-bounces@{1}'.format(self.list_name, self.host_name) - - @property - def join_address(self): - return '{0}-join@{1}'.format(self.list_name, self.host_name) - - @property - def leave_address(self): - return '{0}-leave@{1}'.format(self.list_name, self.host_name) - - @property - def subscribe_address(self): - return '{0}-subscribe@{1}'.format(self.list_name, self.host_name) - - @property - def unsubscribe_address(self): - return '{0}-unsubscribe@{1}'.format(self.list_name, self.host_name) - - def confirm_address(self, cookie): - local_part = expand(config.mta.verp_confirm_format, dict( - address = '{0}-confirm'.format(self.list_name), - cookie = cookie)) - return '{0}@{1}'.format(local_part, self.host_name) - - def __repr__(self): - return ''.format( - self.fqdn_listname, id(self)) diff --git a/mailman/database/mailman.sql b/mailman/database/mailman.sql deleted file mode 100644 index b098ed13b..000000000 --- a/mailman/database/mailman.sql +++ /dev/null @@ -1,208 +0,0 @@ -CREATE TABLE _request ( - id INTEGER NOT NULL, - "key" TEXT, - request_type TEXT, - data_hash TEXT, - mailing_list_id INTEGER, - PRIMARY KEY (id), - CONSTRAINT _request_mailing_list_id_fk FOREIGN KEY(mailing_list_id) REFERENCES mailinglist (id) -); -CREATE TABLE address ( - id INTEGER NOT NULL, - address TEXT, - _original TEXT, - real_name TEXT, - verified_on TIMESTAMP, - registered_on TIMESTAMP, - user_id INTEGER, - preferences_id INTEGER, - PRIMARY KEY (id), - CONSTRAINT address_user_id_fk FOREIGN KEY(user_id) REFERENCES user (id), - CONSTRAINT address_preferences_id_fk FOREIGN KEY(preferences_id) REFERENCES preferences (id) -); -CREATE TABLE language ( - id INTEGER NOT NULL, - code TEXT, - PRIMARY KEY (id) -); -CREATE TABLE mailinglist ( - id INTEGER NOT NULL, - list_name TEXT, - host_name TEXT, - created_at TIMESTAMP, - admin_member_chunksize INTEGER, - hold_and_cmd_autoresponses BLOB, - next_request_id INTEGER, - next_digest_number INTEGER, - admin_responses BLOB, - postings_responses BLOB, - request_responses BLOB, - digest_last_sent_at NUMERIC(10, 2), - one_last_digest BLOB, - volume INTEGER, - last_post_time TIMESTAMP, - accept_these_nonmembers BLOB, - acceptable_aliases BLOB, - admin_immed_notify BOOLEAN, - admin_notify_mchanges BOOLEAN, - administrivia BOOLEAN, - advertised BOOLEAN, - anonymous_list BOOLEAN, - archive BOOLEAN, - archive_private BOOLEAN, - archive_volume_frequency INTEGER, - autorespond_admin BOOLEAN, - autorespond_postings BOOLEAN, - autorespond_requests INTEGER, - autoresponse_admin_text TEXT, - autoresponse_graceperiod TEXT, - autoresponse_postings_text TEXT, - autoresponse_request_text TEXT, - ban_list BLOB, - bounce_info_stale_after TEXT, - bounce_matching_headers TEXT, - bounce_notify_owner_on_disable BOOLEAN, - bounce_notify_owner_on_removal BOOLEAN, - bounce_processing BOOLEAN, - bounce_score_threshold INTEGER, - bounce_unrecognized_goes_to_list_owner BOOLEAN, - bounce_you_are_disabled_warnings INTEGER, - bounce_you_are_disabled_warnings_interval TEXT, - collapse_alternatives BOOLEAN, - convert_html_to_plaintext BOOLEAN, - default_member_moderation BOOLEAN, - description TEXT, - digest_footer TEXT, - digest_header TEXT, - digest_is_default BOOLEAN, - digest_send_periodic BOOLEAN, - digest_size_threshold INTEGER, - digest_volume_frequency INTEGER, - digestable BOOLEAN, - discard_these_nonmembers BLOB, - emergency BOOLEAN, - encode_ascii_prefixes BOOLEAN, - filter_action INTEGER, - filter_content BOOLEAN, - filter_filename_extensions BLOB, - filter_mime_types BLOB, - first_strip_reply_to BOOLEAN, - forward_auto_discards BOOLEAN, - gateway_to_mail BOOLEAN, - gateway_to_news BOOLEAN, - generic_nonmember_action INTEGER, - goodbye_msg TEXT, - header_matches BLOB, - hold_these_nonmembers BLOB, - include_list_post_header BOOLEAN, - include_rfc2369_headers BOOLEAN, - info TEXT, - linked_newsgroup TEXT, - max_days_to_hold INTEGER, - max_message_size INTEGER, - max_num_recipients INTEGER, - member_moderation_action BOOLEAN, - member_moderation_notice TEXT, - mime_is_default_digest BOOLEAN, - moderator_password TEXT, - msg_footer TEXT, - msg_header TEXT, - new_member_options INTEGER, - news_moderation TEXT, - news_prefix_subject_too BOOLEAN, - nntp_host TEXT, - nondigestable BOOLEAN, - nonmember_rejection_notice TEXT, - obscure_addresses BOOLEAN, - pass_filename_extensions BLOB, - pass_mime_types BLOB, - personalize TEXT, - pipeline TEXT, - post_id INTEGER, - preferred_language TEXT, - private_roster BOOLEAN, - real_name TEXT, - reject_these_nonmembers BLOB, - reply_goes_to_list TEXT, - reply_to_address TEXT, - require_explicit_destination BOOLEAN, - respond_to_post_requests BOOLEAN, - scrub_nondigest BOOLEAN, - send_goodbye_msg BOOLEAN, - send_reminders BOOLEAN, - send_welcome_msg BOOLEAN, - start_chain TEXT, - subject_prefix TEXT, - subscribe_auto_approval BLOB, - subscribe_policy INTEGER, - topics BLOB, - topics_bodylines_limit INTEGER, - topics_enabled BOOLEAN, - unsubscribe_policy INTEGER, - welcome_msg TEXT, - PRIMARY KEY (id) -); -CREATE TABLE member ( - id INTEGER NOT NULL, - role TEXT, - mailing_list TEXT, - is_moderated BOOLEAN, - address_id INTEGER, - preferences_id INTEGER, - PRIMARY KEY (id), - CONSTRAINT member_address_id_fk FOREIGN KEY(address_id) REFERENCES address (id), - CONSTRAINT member_preferences_id_fk FOREIGN KEY(preferences_id) REFERENCES preferences (id) -); -CREATE TABLE message ( - id INTEGER NOT NULL, - message_id_hash TEXT, - path TEXT, - message_id TEXT, - PRIMARY KEY (id) -); -CREATE TABLE pended ( - id INTEGER NOT NULL, - token TEXT, - expiration_date TIMESTAMP, - PRIMARY KEY (id) -); -CREATE TABLE pendedkeyvalue ( - id INTEGER NOT NULL, - "key" TEXT, - value TEXT, - pended_id INTEGER, - PRIMARY KEY (id), - CONSTRAINT pendedkeyvalue_pended_id_fk FOREIGN KEY(pended_id) REFERENCES pended (id) -); -CREATE TABLE preferences ( - id INTEGER NOT NULL, - acknowledge_posts BOOLEAN, - hide_address BOOLEAN, - preferred_language TEXT, - receive_list_copy BOOLEAN, - receive_own_postings BOOLEAN, - delivery_mode TEXT, - delivery_status TEXT, - PRIMARY KEY (id) -); -CREATE TABLE user ( - id INTEGER NOT NULL, - real_name TEXT, - password TEXT, - preferences_id INTEGER, - PRIMARY KEY (id), - CONSTRAINT user_preferences_id_fk FOREIGN KEY(preferences_id) REFERENCES preferences (id) -); -CREATE TABLE version ( - id INTEGER NOT NULL, - component TEXT, - version INTEGER, - PRIMARY KEY (id) -); -CREATE INDEX ix__request_mailing_list_id ON _request (mailing_list_id); -CREATE INDEX ix_address_preferences_id ON address (preferences_id); -CREATE INDEX ix_address_user_id ON address (user_id); -CREATE INDEX ix_member_address_id ON member (address_id); -CREATE INDEX ix_member_preferences_id ON member (preferences_id); -CREATE INDEX ix_pendedkeyvalue_pended_id ON pendedkeyvalue (pended_id); -CREATE INDEX ix_user_preferences_id ON user (preferences_id); diff --git a/mailman/database/member.py b/mailman/database/member.py deleted file mode 100644 index 22bf042f6..000000000 --- a/mailman/database/member.py +++ /dev/null @@ -1,105 +0,0 @@ -# Copyright (C) 2007-2009 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 . - -"""Model for members.""" - -from __future__ import absolute_import, unicode_literals - -__metaclass__ = type -__all__ = [ - 'Member', - ] - -from storm.locals import * -from zope.interface import implements - -from mailman.config import config -from mailman.constants import SystemDefaultPreferences -from mailman.database.model import Model -from mailman.database.types import Enum -from mailman.interfaces.member import IMember - - - -class Member(Model): - implements(IMember) - - id = Int(primary=True) - role = Enum() - mailing_list = Unicode() - is_moderated = Bool() - - address_id = Int() - address = Reference(address_id, 'Address.id') - preferences_id = Int() - preferences = Reference(preferences_id, 'Preferences.id') - - def __init__(self, role, mailing_list, address): - self.role = role - self.mailing_list = mailing_list - self.address = address - self.is_moderated = False - - def __repr__(self): - return ''.format( - self.address, self.mailing_list, self.role) - - def _lookup(self, preference): - pref = getattr(self.preferences, preference) - if pref is not None: - return pref - pref = getattr(self.address.preferences, preference) - if pref is not None: - return pref - if self.address.user: - pref = getattr(self.address.user.preferences, preference) - if pref is not None: - return pref - return getattr(SystemDefaultPreferences, preference) - - @property - def acknowledge_posts(self): - return self._lookup('acknowledge_posts') - - @property - def preferred_language(self): - return self._lookup('preferred_language') - - @property - def receive_list_copy(self): - return self._lookup('receive_list_copy') - - @property - def receive_own_postings(self): - return self._lookup('receive_own_postings') - - @property - def delivery_mode(self): - return self._lookup('delivery_mode') - - @property - def delivery_status(self): - return self._lookup('delivery_status') - - @property - def options_url(self): - # XXX Um, this is definitely wrong - return 'http://example.com/' + self.address.address - - def unsubscribe(self): - config.db.store.remove(self.preferences) - config.db.store.remove(self) diff --git a/mailman/database/message.py b/mailman/database/message.py deleted file mode 100644 index e77e11429..000000000 --- a/mailman/database/message.py +++ /dev/null @@ -1,53 +0,0 @@ -# Copyright (C) 2007-2009 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 . - -"""Model for messages.""" - - -from __future__ import absolute_import, unicode_literals - -__metaclass__ = type -__all__ = [ - 'Message', - ] - -from storm.locals import * -from zope.interface import implements - -from mailman.config import config -from mailman.database.model import Model -from mailman.interfaces.messages import IMessage - - - -class Message(Model): - """A message in the message store.""" - - implements(IMessage) - - id = Int(primary=True, default=AutoReload) - message_id = Unicode() - message_id_hash = RawStr() - path = RawStr() - # This is a Messge-ID field representation, not a database row id. - - def __init__(self, message_id, message_id_hash, path): - super(Message, self).__init__() - self.message_id = message_id - self.message_id_hash = message_id_hash - self.path = path - config.db.store.add(self) diff --git a/mailman/database/messagestore.py b/mailman/database/messagestore.py deleted file mode 100644 index a129f47ec..000000000 --- a/mailman/database/messagestore.py +++ /dev/null @@ -1,137 +0,0 @@ -# Copyright (C) 2007-2009 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 . - -"""Model for message stores.""" - - -from __future__ import absolute_import, unicode_literals - -__metaclass__ = type -__all__ = [ - 'MessageStore', - ] - -import os -import errno -import base64 -import hashlib -import cPickle as pickle - -from zope.interface import implements - -from mailman.config import config -from mailman.database.message import Message -from mailman.interfaces.messages import IMessageStore -from mailman.utilities.filesystem import makedirs - - -# It could be very bad if you have already stored files and you change this -# value. We'd need a script to reshuffle and resplit. -MAX_SPLITS = 2 -EMPTYSTRING = '' - - - -class MessageStore: - implements(IMessageStore) - - def add(self, message): - # Ensure that the message has the requisite headers. - message_ids = message.get_all('message-id', []) - if len(message_ids) <> 1: - raise ValueError('Exactly one Message-ID header required') - # Calculate and insert the X-Message-ID-Hash. - message_id = message_ids[0] - # Complain if the Message-ID already exists in the storage. - existing = config.db.store.find(Message, - Message.message_id == message_id).one() - if existing is not None: - raise ValueError( - 'Message ID already exists in message store: {0}'.format( - message_id)) - shaobj = hashlib.sha1(message_id) - hash32 = base64.b32encode(shaobj.digest()) - del message['X-Message-ID-Hash'] - message['X-Message-ID-Hash'] = hash32 - # Calculate the path on disk where we're going to store this message - # object, in pickled format. - parts = [] - split = list(hash32) - while split and len(parts) < MAX_SPLITS: - parts.append(split.pop(0) + split.pop(0)) - parts.append(hash32) - relpath = os.path.join(*parts) - # Store the message in the database. This relies on the database - # providing a unique serial number, but to get this information, we - # have to use a straight insert instead of relying on Elixir to create - # the object. - row = Message(message_id=message_id, - message_id_hash=hash32, - path=relpath) - # Now calculate the full file system path. - path = os.path.join(config.MESSAGES_DIR, relpath) - # Write the file to the path, but catch the appropriate exception in - # case the parent directories don't yet exist. In that case, create - # them and try again. - while True: - try: - with open(path, 'w') as fp: - # -1 says to use the highest protocol available. - pickle.dump(message, fp, -1) - break - except IOError as error: - if error.errno <> errno.ENOENT: - raise - makedirs(os.path.dirname(path)) - return hash32 - - def _get_message(self, row): - path = os.path.join(config.MESSAGES_DIR, row.path) - with open(path) as fp: - return pickle.load(fp) - - def get_message_by_id(self, message_id): - row = config.db.store.find(Message, message_id=message_id).one() - if row is None: - return None - return self._get_message(row) - - def get_message_by_hash(self, message_id_hash): - # It's possible the hash came from a message header, in which case it - # will be a Unicode. However when coming from source code, it may be - # an 8-string. Coerce to the latter if necessary; it must be - # US-ASCII. - if isinstance(message_id_hash, unicode): - message_id_hash = message_id_hash.encode('ascii') - row = config.db.store.find(Message, - message_id_hash=message_id_hash).one() - if row is None: - return None - return self._get_message(row) - - @property - def messages(self): - for row in config.db.store.find(Message): - yield self._get_message(row) - - def delete_message(self, message_id): - row = config.db.store.find(Message, message_id=message_id).one() - if row is None: - raise LookupError(message_id) - path = os.path.join(config.MESSAGES_DIR, row.path) - os.remove(path) - config.db.store.remove(row) diff --git a/mailman/database/model.py b/mailman/database/model.py deleted file mode 100644 index 85fa033c7..000000000 --- a/mailman/database/model.py +++ /dev/null @@ -1,56 +0,0 @@ -# Copyright (C) 2006-2009 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 . - -"""Base class for all database classes.""" - -from __future__ import absolute_import, unicode_literals - -__metaclass__ = type -__all__ = [ - 'Model', - ] - -from storm.properties import PropertyPublisherMeta - - - -class ModelMeta(PropertyPublisherMeta): - """Do more magic on table classes.""" - - _class_registry = set() - - def __init__(self, name, bases, dict): - # Before we let the base class do it's thing, force an __storm_table__ - # property to enforce our table naming convention. - self.__storm_table__ = name.lower() - super(ModelMeta, self).__init__(name, bases, dict) - # Register the model class so that it can be more easily cleared. - # This is required by the test framework. - if name == 'Model': - return - ModelMeta._class_registry.add(self) - - @staticmethod - def _reset(store): - for model_class in ModelMeta._class_registry: - store.find(model_class).remove() - - - -class Model: - """Like Storm's `Storm` subclass, but with a bit extra.""" - __metaclass__ = ModelMeta diff --git a/mailman/database/pending.py b/mailman/database/pending.py deleted file mode 100644 index f4c2057e0..000000000 --- a/mailman/database/pending.py +++ /dev/null @@ -1,177 +0,0 @@ -# Copyright (C) 2007-2009 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 . - -"""Implementations of the IPendable and IPending interfaces.""" - -from __future__ import absolute_import, unicode_literals - -__metaclass__ = type -__all__ = [ - 'Pended', - 'Pendings', - ] - -import sys -import time -import random -import hashlib -import datetime - -from lazr.config import as_timedelta -from storm.locals import * -from zope.interface import implements -from zope.interface.verify import verifyObject - -from mailman.config import config -from mailman.database.model import Model -from mailman.interfaces.pending import ( - IPendable, IPended, IPendedKeyValue, IPendings) - - - -class PendedKeyValue(Model): - """A pended key/value pair, tied to a token.""" - - implements(IPendedKeyValue) - - def __init__(self, key, value): - self.key = key - self.value = value - - id = Int(primary=True) - key = Unicode() - value = Unicode() - pended_id = Int() - - -class Pended(Model): - """A pended event, tied to a token.""" - - implements(IPended) - - def __init__(self, token, expiration_date): - super(Pended, self).__init__() - self.token = token - self.expiration_date = expiration_date - - id = Int(primary=True) - token = RawStr() - expiration_date = DateTime() - key_values = ReferenceSet(id, PendedKeyValue.pended_id) - - - -class UnpendedPendable(dict): - implements(IPendable) - - - -class Pendings: - """Implementation of the IPending interface.""" - - implements(IPendings) - - def add(self, pendable, lifetime=None): - verifyObject(IPendable, pendable) - # Calculate the token and the lifetime. - if lifetime is None: - lifetime = as_timedelta(config.mailman.pending_request_life) - # Calculate a unique token. Algorithm vetted by the Timbot. time() - # has high resolution on Linux, clock() on Windows. random gives us - # about 45 bits in Python 2.2, 53 bits on Python 2.3. The time and - # clock values basically help obscure the random number generator, as - # does the hash calculation. The integral parts of the time values - # are discarded because they're the most predictable bits. - for attempts in range(3): - now = time.time() - x = random.random() + now % 1.0 + time.clock() % 1.0 - # Use sha1 because it produces shorter strings. - token = hashlib.sha1(repr(x)).hexdigest() - # In practice, we'll never get a duplicate, but we'll be anal - # about checking anyway. - if config.db.store.find(Pended, token=token).count() == 0: - break - else: - raise AssertionError('Could not find a valid pendings token') - # Create the record, and then the individual key/value pairs. - pending = Pended( - token=token, - expiration_date=datetime.datetime.now() + lifetime) - for key, value in pendable.items(): - if isinstance(key, str): - key = unicode(key, 'utf-8') - if isinstance(value, str): - value = unicode(value, 'utf-8') - elif type(value) is int: - value = '__builtin__.int\1%s' % value - elif type(value) is float: - value = '__builtin__.float\1%s' % value - elif type(value) is bool: - value = '__builtin__.bool\1%s' % value - elif type(value) is list: - # We expect this to be a list of strings. - value = ('mailman.database.pending.unpack_list\1' + - '\2'.join(value)) - keyval = PendedKeyValue(key=key, value=value) - pending.key_values.add(keyval) - config.db.store.add(pending) - return token - - def confirm(self, token, expunge=True): - store = config.db.store - pendings = store.find(Pended, token=token) - if pendings.count() == 0: - return None - assert pendings.count() == 1, ( - 'Unexpected token count: {0}'.format(pendings.count())) - pending = pendings[0] - pendable = UnpendedPendable() - # Find all PendedKeyValue entries that are associated with the pending - # object's ID. Watch out for type conversions. - for keyvalue in store.find(PendedKeyValue, - PendedKeyValue.pended_id == pending.id): - if keyvalue.value is not None and '\1' in keyvalue.value: - typename, value = keyvalue.value.split('\1', 1) - package, classname = typename.rsplit('.', 1) - __import__(package) - module = sys.modules[package] - pendable[keyvalue.key] = getattr(module, classname)(value) - else: - pendable[keyvalue.key] = keyvalue.value - if expunge: - store.remove(keyvalue) - if expunge: - store.remove(pending) - return pendable - - def evict(self): - store = config.db.store - now = datetime.datetime.now() - for pending in store.find(Pended): - if pending.expiration_date < now: - # Find all PendedKeyValue entries that are associated with the - # pending object's ID. - q = store.find(PendedKeyValue, - PendedKeyValue.pended_id == pending.id) - for keyvalue in q: - store.remove(keyvalue) - store.remove(pending) - - - -def unpack_list(value): - return value.split('\2') diff --git a/mailman/database/preferences.py b/mailman/database/preferences.py deleted file mode 100644 index f3ee55673..000000000 --- a/mailman/database/preferences.py +++ /dev/null @@ -1,50 +0,0 @@ -# Copyright (C) 2006-2009 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 . - -"""Model for preferences.""" - -from __future__ import absolute_import, unicode_literals - -__metaclass__ = type -__all__ = [ - 'Preferences', - ] - - -from storm.locals import * -from zope.interface import implements - -from mailman.database.model import Model -from mailman.database.types import Enum -from mailman.interfaces.preferences import IPreferences - - - -class Preferences(Model): - implements(IPreferences) - - id = Int(primary=True) - acknowledge_posts = Bool() - hide_address = Bool() - preferred_language = Unicode() - receive_list_copy = Bool() - receive_own_postings = Bool() - delivery_mode = Enum() - delivery_status = Enum() - - def __repr__(self): - return ''.format(id(self)) diff --git a/mailman/database/requests.py b/mailman/database/requests.py deleted file mode 100644 index 249feb6b6..000000000 --- a/mailman/database/requests.py +++ /dev/null @@ -1,138 +0,0 @@ -# Copyright (C) 2007-2009 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 . - -"""Implementations of the IRequests and IListRequests interfaces.""" - -from __future__ import absolute_import, unicode_literals - -__metaclass__ = type -__all__ = [ - 'Requests', - ] - - -from datetime import timedelta -from storm.locals import * -from zope.interface import implements - -from mailman.config import config -from mailman.database.model import Model -from mailman.database.types import Enum -from mailman.interfaces.pending import IPendable -from mailman.interfaces.requests import IListRequests, IRequests, RequestType - - - -class DataPendable(dict): - implements(IPendable) - - - -class ListRequests: - implements(IListRequests) - - def __init__(self, mailing_list): - self.mailing_list = mailing_list - - @property - def count(self): - return config.db.store.find( - _Request, mailing_list=self.mailing_list).count() - - def count_of(self, request_type): - return config.db.store.find( - _Request, - mailing_list=self.mailing_list, request_type=request_type).count() - - @property - def held_requests(self): - results = config.db.store.find( - _Request, mailing_list=self.mailing_list) - for request in results: - yield request - - def of_type(self, request_type): - results = config.db.store.find( - _Request, - mailing_list=self.mailing_list, request_type=request_type) - for request in results: - yield request - - def hold_request(self, request_type, key, data=None): - if request_type not in RequestType: - raise TypeError(request_type) - if data is None: - data_hash = None - else: - # We're abusing the pending database as a way of storing arbitrary - # key/value pairs, where both are strings. This isn't ideal but - # it lets us get auxiliary data almost for free. We may need to - # lock this down more later. - pendable = DataPendable() - pendable.update(data) - token = config.db.pendings.add(pendable, timedelta(days=5000)) - data_hash = token - request = _Request(key, request_type, self.mailing_list, data_hash) - config.db.store.add(request) - return request.id - - def get_request(self, request_id): - result = config.db.store.get(_Request, request_id) - if result is None: - return None - if result.data_hash is None: - return result.key, result.data_hash - pendable = config.db.pendings.confirm(result.data_hash, expunge=False) - data = dict() - data.update(pendable) - return result.key, data - - def delete_request(self, request_id): - request = config.db.store.get(_Request, request_id) - if request is None: - raise KeyError(request_id) - # Throw away the pended data. - config.db.pendings.confirm(request.data_hash) - config.db.store.remove(request) - - - -class Requests: - implements(IRequests) - - def get_list_requests(self, mailing_list): - return ListRequests(mailing_list) - - - -class _Request(Model): - """Table for mailing list hold requests.""" - - id = Int(primary=True, default=AutoReload) - key = Unicode() - request_type = Enum() - data_hash = RawStr() - - mailing_list_id = Int() - mailing_list = Reference(mailing_list_id, 'MailingList.id') - - def __init__(self, key, request_type, mailing_list, data_hash): - super(_Request, self).__init__() - self.key = key - self.request_type = request_type - self.mailing_list = mailing_list - self.data_hash = data_hash diff --git a/mailman/database/roster.py b/mailman/database/roster.py deleted file mode 100644 index fc0a24c7d..000000000 --- a/mailman/database/roster.py +++ /dev/null @@ -1,270 +0,0 @@ -# Copyright (C) 2007-2009 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 . - -"""An implementation of an IRoster. - -These are hard-coded rosters which know how to filter a set of members to find -the ones that fit a particular role. These are used as the member, owner, -moderator, and administrator roster filters. -""" - -from __future__ import absolute_import, unicode_literals - -__metaclass__ = type -__all__ = [ - 'AdministratorRoster', - 'DigestMemberRoster', - 'MemberRoster', - 'Memberships', - 'ModeratorRoster', - 'OwnerRoster', - 'RegularMemberRoster', - 'Subscribers', - ] - - -from storm.locals import * -from zope.interface import implements - -from mailman.config import config -from mailman.database.address import Address -from mailman.database.member import Member -from mailman.interfaces.member import DeliveryMode, MemberRole -from mailman.interfaces.roster import IRoster - - - -class AbstractRoster: - """An abstract IRoster class. - - This class takes the simple approach of implemented the 'users' and - 'addresses' properties in terms of the 'members' property. This may not - be the most efficient way, but it works. - - This requires that subclasses implement the 'members' property. - """ - implements(IRoster) - - role = None - - def __init__(self, mlist): - self._mlist = mlist - - @property - def members(self): - for member in config.db.store.find( - Member, - mailing_list=self._mlist.fqdn_listname, - role=self.role): - yield member - - @property - def users(self): - # Members are linked to addresses, which in turn are linked to users. - # So while the 'members' attribute does most of the work, we have to - # keep a set of unique users. It's possible for the same user to be - # subscribed to a mailing list multiple times with different - # addresses. - users = set(member.address.user for member in self.members) - for user in users: - yield user - - @property - def addresses(self): - # Every Member is linked to exactly one address so the 'members' - # attribute does most of the work. - for member in self.members: - yield member.address - - def get_member(self, address): - results = config.db.store.find( - Member, - Member.mailing_list == self._mlist.fqdn_listname, - Member.role == self.role, - Address.address == address, - Member.address_id == Address.id) - if results.count() == 0: - return None - elif results.count() == 1: - return results[0] - else: - raise AssertionError( - 'Too many matching member results: {0}'.format( - results.count())) - - - -class MemberRoster(AbstractRoster): - """Return all the members of a list.""" - - name = 'member' - role = MemberRole.member - - - -class OwnerRoster(AbstractRoster): - """Return all the owners of a list.""" - - name = 'owner' - role = MemberRole.owner - - - -class ModeratorRoster(AbstractRoster): - """Return all the owners of a list.""" - - name = 'moderator' - role = MemberRole.moderator - - - -class AdministratorRoster(AbstractRoster): - """Return all the administrators of a list.""" - - name = 'administrator' - - @property - def members(self): - # Administrators are defined as the union of the owners and the - # moderators. - members = config.db.store.find( - Member, - Member.mailing_list == self._mlist.fqdn_listname, - Or(Member.role == MemberRole.owner, - Member.role == MemberRole.moderator)) - for member in members: - yield member - - def get_member(self, address): - results = config.db.store.find( - Member, - Member.mailing_list == self._mlist.fqdn_listname, - Or(Member.role == MemberRole.moderator, - Member.role == MemberRole.owner), - Address.address == address, - Member.address_id == Address.id) - if results.count() == 0: - return None - elif results.count() == 1: - return results[0] - else: - raise AssertionError( - 'Too many matching member results: {0}'.format(results)) - - - -class RegularMemberRoster(AbstractRoster): - """Return all the regular delivery members of a list.""" - - name = 'regular_members' - - @property - def members(self): - # Query for all the Members which have a role of MemberRole.member and - # are subscribed to this mailing list. Then return only those members - # that have a regular delivery mode. - for member in config.db.store.find( - Member, - mailing_list=self._mlist.fqdn_listname, - role=MemberRole.member): - if member.delivery_mode == DeliveryMode.regular: - yield member - - - -_digest_modes = ( - DeliveryMode.mime_digests, - DeliveryMode.plaintext_digests, - DeliveryMode.summary_digests, - ) - - - -class DigestMemberRoster(AbstractRoster): - """Return all the regular delivery members of a list.""" - - name = 'digest_members' - - @property - def members(self): - # Query for all the Members which have a role of MemberRole.member and - # are subscribed to this mailing list. Then return only those members - # that have one of the digest delivery modes. - for member in config.db.store.find( - Member, - mailing_list=self._mlist.fqdn_listname, - role=MemberRole.member): - if member.delivery_mode in _digest_modes: - yield member - - - -class Subscribers(AbstractRoster): - """Return all subscribed members regardless of their role.""" - - name = 'subscribers' - - @property - def members(self): - for member in config.db.store.find( - Member, - mailing_list=self._mlist.fqdn_listname): - yield member - - - -class Memberships: - """A roster of a single user's memberships.""" - - implements(IRoster) - - name = 'memberships' - - def __init__(self, user): - self._user = user - - @property - def members(self): - results = config.db.store.find( - Member, - Address.user_id == self._user.id, - Member.address_id == Address.id) - for member in results: - yield member - - @property - def users(self): - yield self._user - - @property - def addresses(self): - for address in self._user.addresses: - yield address - - def get_member(self, address): - results = config.db.store.find( - Member, - Member.address_id == Address.id, - Address.user_id == self._user.id) - if results.count() == 0: - return None - elif results.count() == 1: - return results[0] - else: - raise AssertionError( - 'Too many matching member results: {0}'.format( - results.count())) diff --git a/mailman/database/transaction.py b/mailman/database/transaction.py deleted file mode 100644 index d42562389..000000000 --- a/mailman/database/transaction.py +++ /dev/null @@ -1,53 +0,0 @@ -# Copyright (C) 2006-2009 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 . - -"""Transactional support.""" - -from __future__ import absolute_import, unicode_literals - -__metaclass__ = type -__all__ = [ - 'txn', - ] - - -from mailman.config import config - - - -class txn(object): - """Decorator for transactional support. - - When the function this decorator wraps exits cleanly, the current - transaction is committed. When it exits uncleanly (i.e. because of an - exception, the transaction is aborted. - - Either way, the current transaction is completed. - """ - def __init__(self, function): - self._function = function - - def __get__(self, obj, type=None): - def wrapper(*args, **kws): - try: - rtn = self._function(obj, *args, **kws) - config.db.commit() - return rtn - except: - config.db.abort() - raise - return wrapper diff --git a/mailman/database/types.py b/mailman/database/types.py deleted file mode 100644 index 2f901fe49..000000000 --- a/mailman/database/types.py +++ /dev/null @@ -1,64 +0,0 @@ -# Copyright (C) 2007-2009 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 . - -"""Storm type conversions.""" - - -from __future__ import absolute_import, unicode_literals - -__metaclass__ = type -__all__ = [ - 'Enum', - ] - - -import sys - -from storm.properties import SimpleProperty -from storm.variables import Variable - - - -class _EnumVariable(Variable): - """Storm variable.""" - - def parse_set(self, value, from_db): - if value is None: - return None - if not from_db: - return value - path, intvalue = value.rsplit(':', 1) - modulename, classname = path.rsplit('.', 1) - __import__(modulename) - cls = getattr(sys.modules[modulename], classname) - return cls[int(intvalue)] - - def parse_get(self, value, to_db): - if value is None: - return None - if not to_db: - return value - return '{0}.{1}:{2}'.format( - value.enumclass.__module__, - value.enumclass.__name__, - int(value)) - - -class Enum(SimpleProperty): - """Custom munepy.Enum type for Storm.""" - - variable_class = _EnumVariable diff --git a/mailman/database/user.py b/mailman/database/user.py deleted file mode 100644 index 23701686b..000000000 --- a/mailman/database/user.py +++ /dev/null @@ -1,94 +0,0 @@ -# Copyright (C) 2007-2009 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 . - -"""Model for users.""" - -from __future__ import absolute_import, unicode_literals - -__metaclass__ = type -__all__ = [ - 'User', - ] - -from storm.locals import * -from zope.interface import implements - -from mailman.config import config -from mailman.database.model import Model -from mailman.database.address import Address -from mailman.database.preferences import Preferences -from mailman.database.roster import Memberships -from mailman.interfaces.address import ( - AddressAlreadyLinkedError, AddressNotLinkedError) -from mailman.interfaces.user import IUser - - - -class User(Model): - """Mailman users.""" - - implements(IUser) - - id = Int(primary=True) - real_name = Unicode() - password = Unicode() - - addresses = ReferenceSet(id, 'Address.user_id') - preferences_id = Int() - preferences = Reference(preferences_id, 'Preferences.id') - - def __repr__(self): - return ''.format(self.real_name, id(self)) - - def link(self, address): - """See `IUser`.""" - if address.user is not None: - raise AddressAlreadyLinkedError(address) - address.user = self - - def unlink(self, address): - """See `IUser`.""" - if address.user is None: - raise AddressNotLinkedError(address) - address.user = None - - def controls(self, address): - """See `IUser`.""" - found = config.db.store.find(Address, address=address) - if found.count() == 0: - return False - assert found.count() == 1, 'Unexpected count' - return found[0].user is self - - def register(self, address, real_name=None): - """See `IUser`.""" - # First, see if the address already exists - addrobj = config.db.store.find(Address, address=address).one() - if addrobj is None: - if real_name is None: - real_name = '' - addrobj = Address(address=address, real_name=real_name) - addrobj.preferences = Preferences() - # Link the address to the user if it is not already linked. - if addrobj.user is not None: - raise AddressAlreadyLinkedError(addrobj) - addrobj.user = self - return addrobj - - @property - def memberships(self): - return Memberships(self) diff --git a/mailman/database/usermanager.py b/mailman/database/usermanager.py deleted file mode 100644 index 3b0c8b534..000000000 --- a/mailman/database/usermanager.py +++ /dev/null @@ -1,103 +0,0 @@ -# Copyright (C) 2007-2009 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 . - -"""A user manager.""" - -from __future__ import absolute_import, unicode_literals - -__metaclass__ = type -__all__ = [ - 'UserManager', - ] - -from zope.interface import implements - -from mailman.config import config -from mailman.database.address import Address -from mailman.database.preferences import Preferences -from mailman.database.user import User -from mailman.interfaces.address import ExistingAddressError -from mailman.interfaces.usermanager import IUserManager - - - -class UserManager(object): - implements(IUserManager) - - def create_user(self, address=None, real_name=None): - user = User() - user.real_name = ('' if real_name is None else real_name) - if address: - addrobj = Address(address, user.real_name) - addrobj.preferences = Preferences() - user.link(addrobj) - user.preferences = Preferences() - config.db.store.add(user) - return user - - def delete_user(self, user): - config.db.store.remove(user) - - @property - def users(self): - for user in config.db.store.find(User): - yield user - - def get_user(self, address): - addresses = config.db.store.find(Address, address=address.lower()) - if addresses.count() == 0: - return None - elif addresses.count() == 1: - return addresses[0].user - else: - raise AssertionError('Unexpected query count') - - def create_address(self, address, real_name=None): - addresses = config.db.store.find(Address, address=address.lower()) - if addresses.count() == 1: - found = addresses[0] - raise ExistingAddressError(found.original_address) - assert addresses.count() == 0, 'Unexpected results' - if real_name is None: - real_name = '' - # It's okay not to lower case the 'address' argument because the - # constructor will do the right thing. - address = Address(address, real_name) - address.preferences = Preferences() - config.db.store.add(address) - return address - - def delete_address(self, address): - # If there's a user controlling this address, it has to first be - # unlinked before the address can be deleted. - if address.user: - address.user.unlink(address) - config.db.store.remove(address) - - def get_address(self, address): - addresses = config.db.store.find(Address, address=address.lower()) - if addresses.count() == 0: - return None - elif addresses.count() == 1: - return addresses[0] - else: - raise AssertionError('Unexpected query count') - - @property - def addresses(self): - for address in config.db.store.find(Address): - yield address diff --git a/mailman/database/version.py b/mailman/database/version.py deleted file mode 100644 index d15065395..000000000 --- a/mailman/database/version.py +++ /dev/null @@ -1,40 +0,0 @@ -# Copyright (C) 2007-2009 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 . - -"""Model class for version numbers.""" - -from __future__ import absolute_import, unicode_literals - -__metaclass__ = type -__all__ = [ - 'Version', - ] - -from storm.locals import * -from mailman.database.model import Model - - - -class Version(Model): - id = Int(primary=True) - component = Unicode() - version = Int() - - def __init__(self, component, version): - super(Version, self).__init__() - self.component = component - self.version = version diff --git a/mailman/docs/__init__.py b/mailman/docs/__init__.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/mailman/docs/addresses.txt b/mailman/docs/addresses.txt deleted file mode 100644 index 9eccb2673..000000000 --- a/mailman/docs/addresses.txt +++ /dev/null @@ -1,231 +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. - - >>> usermgr = config.db.user_manager - - -Creating addresses ------------------- - -Addresses are created directly through the user manager, which starts out with -no addresses. - - >>> sorted(address.address for address in usermgr.addresses) - [] - -Creating an unlinked email address is straightforward. - - >>> address_1 = usermgr.create_address(u'aperson@example.com') - >>> sorted(address.address for address in usermgr.addresses) - [u'aperson@example.com'] - -However, such addresses have no real name. - - >>> address_1.real_name - u'' - -You can also create an email address object with a real name. - - >>> address_2 = usermgr.create_address( - ... u'bperson@example.com', u'Ben Person') - >>> sorted(address.address for address in usermgr.addresses) - [u'aperson@example.com', u'bperson@example.com'] - >>> sorted(address.real_name for address in usermgr.addresses) - [u'', u'Ben Person'] - -The str() of the address is the RFC 2822 preferred originator format, while -the repr() carries more information. - - >>> str(address_2) - 'Ben Person ' - >>> repr(address_2) - ' [not verified] at 0x...>' - -You can assign real names to existing addresses. - - >>> address_1.real_name = u'Anne Person' - >>> sorted(address.real_name for address in usermgr.addresses) - [u'Anne Person', u'Ben Person'] - -These addresses are not linked to users, and can be seen by searching the user -manager for an associated user. - - >>> print usermgr.get_user(u'aperson@example.com') - None - >>> print usermgr.get_user(u'bperson@example.com') - None - -You can create email addresses that are linked to users by using a different -interface. - - >>> user_1 = usermgr.create_user(u'cperson@example.com', u'Claire Person') - >>> sorted(address.address for address in user_1.addresses) - [u'cperson@example.com'] - >>> sorted(address.address for address in usermgr.addresses) - [u'aperson@example.com', u'bperson@example.com', u'cperson@example.com'] - >>> sorted(address.real_name for address in usermgr.addresses) - [u'Anne Person', u'Ben Person', u'Claire Person'] - -And now you can find the associated user. - - >>> print usermgr.get_user(u'aperson@example.com') - None - >>> print usermgr.get_user(u'bperson@example.com') - None - >>> usermgr.get_user(u'cperson@example.com') - - - -Deleting addresses ------------------- - -You can remove an unlinked address from the user manager. - - >>> usermgr.delete_address(address_1) - >>> sorted(address.address for address in usermgr.addresses) - [u'bperson@example.com', u'cperson@example.com'] - >>> sorted(address.real_name for address in usermgr.addresses) - [u'Ben Person', u'Claire Person'] - -Deleting a linked address does not delete the user, but it does unlink the -address from the user. - - >>> sorted(address.address for address in user_1.addresses) - [u'cperson@example.com'] - >>> user_1.controls(u'cperson@example.com') - True - >>> address_3 = list(user_1.addresses)[0] - >>> usermgr.delete_address(address_3) - >>> sorted(address.address for address in user_1.addresses) - [] - >>> user_1.controls(u'cperson@example.com') - False - >>> sorted(address.address for address in usermgr.addresses) - [u'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. Neither date is set by default. - - >>> address_4 = usermgr.create_address( - ... u'dperson@example.com', u'Dan Person') - >>> print address_4.registered_on - None - >>> print address_4.verified_on - None - -The registered date takes a Python datetime object. - - >>> from datetime import datetime - >>> address_4.registered_on = datetime(2007, 5, 8, 22, 54, 1) - >>> print address_4.registered_on - 2007-05-08 22:54:01 - >>> print address_4.verified_on - None - -And of course, you can also set the validation date. - - >>> address_4.verified_on = datetime(2007, 5, 13, 22, 54, 1) - >>> print address_4.registered_on - 2007-05-08 22:54:01 - >>> print address_4.verified_on - 2007-05-13 22:54:01 - - -Subscriptions -------------- - -Addresses get subscribed to mailing lists, not users. When the address is -subscribed, a role is specified. - - >>> address_5 = usermgr.create_address( - ... u'eperson@example.com', u'Elly Person') - >>> mlist = config.db.list_manager.create(u'_xtext@example.com') - >>> from mailman.interfaces.member import MemberRole - >>> address_5.subscribe(mlist, MemberRole.owner) - on - _xtext@example.com as MemberRole.owner> - >>> address_5.subscribe(mlist, MemberRole.member) - on - _xtext@example.com as MemberRole.member> - -Now Elly is both an owner and a member of the mailing list. - - >>> sorted(mlist.owners.members) - [ on - _xtext@example.com as MemberRole.owner>] - >>> sorted(mlist.moderators.members) - [] - >>> sorted(mlist.administrators.members) - [ on - _xtext@example.com as MemberRole.owner>] - >>> sorted(mlist.members.members) - [ on - _xtext@example.com as MemberRole.member>] - >>> sorted(mlist.regular_members.members) - [ on - _xtext@example.com as MemberRole.member>] - >>> sorted(mlist.digest_members.members) - [] - - -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 = usermgr.create_address( - ... u'FPERSON@example.com', u'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. - - >>> str(address_6) - 'Frank Person ' - >>> 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. - - >>> address_6.address - u'fperson@example.com' - >>> address_6.original_address - u'FPERSON@example.com' - -Because addresses are case-insensitive for all other purposes, you cannot -create an address that differs only in case. - - >>> usermgr.create_address(u'fperson@example.com') - Traceback (most recent call last): - ... - ExistingAddressError: FPERSON@example.com - >>> usermgr.create_address(u'fperson@EXAMPLE.COM') - Traceback (most recent call last): - ... - ExistingAddressError: FPERSON@example.com - >>> usermgr.create_address(u'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. - - >>> usermgr.get_address(u'fperson@example.com').address - u'fperson@example.com' - >>> usermgr.get_address(u'FPERSON@example.com').address - u'fperson@example.com' diff --git a/mailman/docs/archivers.txt b/mailman/docs/archivers.txt deleted file mode 100644 index ef36a25ac..000000000 --- a/mailman/docs/archivers.txt +++ /dev/null @@ -1,184 +0,0 @@ -Archivers -========= - -Mailman supports pluggable archivers, and it comes with several default -archivers. - - >>> from mailman.app.lifecycle import create_list - >>> mlist = create_list(u'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. - - >>> archiver.archive_message(mlist, msg) - - >>> from mailman.queue.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= - MIME-Version: 1.0 - Content-Type: text/plain; charset="us-ascii" - Content-Transfer-Encoding: 7bit - Sender: test-bounces@example.com - Errors-To: test-bounces@example.com - X-Peer: 127.0.0.1:... - X-MailFrom: test-bounces@example.com - X-RcptTo: archive@mail-archive.dev - - Here is an archived message. - _______________________________________________ - Test mailing list - test@example.com - http://lists.example.com/listinfo/test@example.com - - >>> 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 -------- - -The 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 - ... diff --git a/mailman/docs/bounces.txt b/mailman/docs/bounces.txt deleted file mode 100644 index 9e8bcd23b..000000000 --- a/mailman/docs/bounces.txt +++ /dev/null @@ -1,107 +0,0 @@ -Bounces -======= - -An important feature of Mailman is automatic bounce process. - -XXX Many more converted tests go here. - - -Bounces, or message rejection ------------------------------ - -Mailman can also 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 = config.db.list_manager.create(u'_xtest@example.com') - >>> mlist.preferred_language = u'en' - -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 messageauthor. - - >>> switchboard = config.switchboards['virgin'] - >>> from mailman.app.bounces import bounce_message - >>> bounce_message(mlist, msg) - >>> len(switchboard.files) - 1 - >>> filebase = switchboard.files[0] - >>> qmsg, qmsgdata = switchboard.dequeue(filebase) - >>> switchboard.finish(filebase) - >>> print qmsg.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) - >>> len(switchboard.files) - 1 - >>> filebase = switchboard.files[0] - >>> qmsg, qmsgdata = switchboard.dequeue(filebase) - >>> switchboard.finish(filebase) - >>> print qmsg.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/mailman/docs/chains.txt b/mailman/docs/chains.txt deleted file mode 100644 index b6e75e6e1..000000000 --- a/mailman/docs/chains.txt +++ /dev/null @@ -1,345 +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. - - >>> from zope.interface.verify import verifyObject - >>> from mailman.interfaces.chain import IChain - >>> chain = config.chains['discard'] - >>> verifyObject(IChain, chain) - True - >>> print chain.name - discard - >>> print chain.description - Discard a message and stop processing. - - >>> from mailman.app.lifecycle import create_list - >>> mlist = create_list(u'_xtest@example.com') - >>> msg = message_from_string("""\ - ... From: aperson@example.com - ... To: _xtest@example.com - ... Subject: My first post - ... Message-ID: - ... - ... An important message. - ... """) - - >>> from mailman.core.chains import process - - # XXX This checks the vette log file because there is no other evidence - # that this chain has done anything. - >>> import os - >>> fp = open(os.path.join(config.LOG_DIR, 'vette')) - >>> file_pos = fp.tell() - >>> process(mlist, msg, {}, 'discard') - >>> fp.seek(file_pos) - >>> print 'LOG:', fp.read() - LOG: ... DISCARD: - - - -The Reject chain ----------------- - -The Reject chain bounces the message back to the original sender, and logs -this action. - - >>> chain = config.chains['reject'] - >>> verifyObject(IChain, chain) - True - >>> print chain.name - reject - >>> print chain.description - Reject/bounce a message and stop processing. - >>> file_pos = fp.tell() - >>> process(mlist, msg, {}, 'reject') - >>> fp.seek(file_pos) - >>> print 'LOG:', fp.read() - LOG: ... REJECT: - -The bounce message is now sitting in the Virgin queue. - - >>> virginq = config.switchboards['virgin'] - >>> len(virginq.files) - 1 - >>> qmsg, qdata = virginq.dequeue(virginq.files[0]) - >>> print qmsg.as_string() - Subject: My first post - From: _xtest-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: _xtest@example.com - Subject: My first post - Message-ID: - - An important message. - - ... - - -The Hold Chain --------------- - -The Hold chain places the message into the admin 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'] - >>> verifyObject(IChain, chain) - True - >>> print chain.name - hold - >>> print chain.description - Hold a message and stop processing. - - >>> file_pos = fp.tell() - >>> process(mlist, msg, {}, 'hold') - >>> fp.seek(file_pos) - >>> print 'LOG:', fp.read() - LOG: ... HOLD: _xtest@example.com post from aperson@example.com held, - message-id=: n/a - - -There are now two messages in the Virgin queue, one to the list moderators and -one to the original author. - - >>> len(virginq.files) - 2 - >>> qfiles = [] - >>> for filebase in virginq.files: - ... qmsg, qdata = virginq.dequeue(filebase) - ... virginq.finish(filebase) - ... qfiles.append(qmsg) - >>> from operator import itemgetter - >>> qfiles.sort(key=itemgetter('to')) - -This message is addressed to the mailing list moderators. - - >>> print qfiles[0].as_string() - Subject: _xtest@example.com post from aperson@example.com requires approval - From: _xtest-owner@example.com - To: _xtest-owner@example.com - MIME-Version: 1.0 - ... - As list administrator, your authorization is requested for the - following mailing list posting: - - List: _xtest@example.com - From: aperson@example.com - Subject: My first post - Reason: XXX - - At your convenience, visit: - - http://lists.example.com/admindb/_xtest@example.com - - to approve or deny the request. - - ... - Content-Type: message/rfc822 - MIME-Version: 1.0 - - From: aperson@example.com - To: _xtest@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 ... - Sender: _xtest-request@example.com - From: _xtest-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 qfiles[1].as_string() - MIME-Version: 1.0 - Content-Type: text/plain; charset="us-ascii" - Content-Transfer-Encoding: 7bit - Subject: Your message to _xtest@example.com awaits moderator approval - From: _xtest-bounces@example.com - To: aperson@example.com - ... - Your mail to '_xtest@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/_xtest@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 qfiles[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' - >>> data = config.db.pendings.confirm(cookie) - >>> sorted(data.items()) - [(u'id', ...), (u'type', u'held message')] - -The message itself is held in the message store. - - >>> rkey, rdata = config.db.requests.get_list_requests(mlist).get_request( - ... data['id']) - >>> msg = config.db.message_store.get_message_by_id( - ... rdata['_mod_message_id']) - >>> print msg.as_string() - From: aperson@example.com - To: _xtest@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 'prep' queue, where it will be -processed and sent on to the list membership. - - >>> chain = config.chains['accept'] - >>> verifyObject(IChain, chain) - True - >>> print chain.name - accept - >>> print chain.description - Accept a message. - >>> file_pos = fp.tell() - >>> process(mlist, msg, {}, 'accept') - >>> fp.seek(file_pos) - >>> print 'LOG:', fp.read() - LOG: ... ACCEPT: - - >>> pipelineq = config.switchboards['pipeline'] - >>> len(pipelineq.files) - 1 - >>> qmsg, qdata = pipelineq.dequeue(pipelineq.files[0]) - >>> print qmsg.as_string() - From: aperson@example.com - To: _xtest@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, providing functionality similar to -the Hold handler from previous versions of Mailman. - - >>> chain = config.chains['built-in'] - >>> verifyObject(IChain, chain) - True - >>> print chain.name - built-in - >>> print chain.description - The built-in moderation chain. - -The previously created message is innocuous enough that it should pass through -all default rules. This message will end up in the pipeline queue. - - >>> file_pos = fp.tell() - >>> process(mlist, msg, {}) - >>> fp.seek(file_pos) - >>> print 'LOG:', fp.read() - LOG: ... ACCEPT: - - >>> qmsg, qdata = pipelineq.dequeue(pipelineq.files[0]) - >>> print qmsg.as_string() - From: aperson@example.com - To: _xtest@example.com - Subject: My first post - Message-ID: - X-Message-ID-Hash: RXJU4JL6N2OUN3OYMXXPPSCR7P7JE2BW - X-Mailman-Rule-Misses: approved; emergency; loop; administrivia; - implicit-dest; - max-recipients; max-size; news-moderation; no-subject; - suspicious-header - - An important message. - - -In addition, the message metadata now contains lists of all rules that have -hit and all rules that have missed. - - >>> sorted(qdata['rule_hits']) - [] - >>> for rule_name in sorted(qdata['rule_misses']): - ... print rule_name - administrivia - approved - emergency - implicit-dest - loop - max-recipients - max-size - news-moderation - no-subject - suspicious-header diff --git a/mailman/docs/domains.txt b/mailman/docs/domains.txt deleted file mode 100644 index b71689520..000000000 --- a/mailman/docs/domains.txt +++ /dev/null @@ -1,46 +0,0 @@ -Domains -======= - -Domains are how Mailman interacts with email host names and web host names. -Generally, new domains are registered in the mailman.cfg configuration file. -We simulate that here by pushing new configurations. - - >>> config.push('example.org', """ - ... [domain.example_dot_org] - ... email_host: example.org - ... base_url: https://mail.example.org - ... description: The example domain - ... contact_address: postmaster@mail.example.org - ... """) - - >>> domain = config.domains['example.org'] - >>> print domain.email_host - example.org - >>> print domain.base_url - https://mail.example.org - >>> print domain.description - The example domain - >>> print domain.contact_address - postmaster@mail.example.org - >>> print domain.url_host - mail.example.org - - -Confirmation tokens -------------------- - -Confirmation tokens can be added to either the email confirmation address... - - >>> print domain.confirm_address('xyz') - confirm-xyz@example.org - -...or the confirmation url. - - >>> print domain.confirm_url('abc') - https://mail.example.org/confirm/abc - - -Clean up --------- - - >>> config.pop('example.org') diff --git a/mailman/docs/languages.txt b/mailman/docs/languages.txt deleted file mode 100644 index 775b933e8..000000000 --- a/mailman/docs/languages.txt +++ /dev/null @@ -1,104 +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 zope.interface.verify import verifyObject - >>> from mailman.interfaces.languages import ILanguageManager - >>> from mailman.languages import LanguageManager - >>> mgr = LanguageManager() - >>> verifyObject(ILanguageManager, mgr) - True - -A language manager keeps track of the languages it knows about as well as the -languages which are enabled. By default, none are known or enabled. - - >>> sorted(mgr.known_codes) - [] - >>> sorted(mgr.enabled_codes) - [] - -The language manager also keeps track of information for each known language, -but you obviously can't get information for an unknown language. - - >>> mgr.get_description('en') - Traceback (most recent call last): - ... - KeyError: 'en' - >>> mgr.get_charset('en') - Traceback (most recent call last): - ... - KeyError: 'en' - - -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_language('en', 'English', 'us-ascii') - >>> mgr.add_language('it', 'Italian', 'iso-8859-1') - -By default, added languages are also enabled. - - >>> sorted(mgr.known_codes) - ['en', 'it'] - >>> sorted(mgr.enabled_codes) - ['en', 'it'] - -And you can get information for all known languages. - - >>> mgr.get_description('en') - 'English' - >>> mgr.get_charset('en') - 'us-ascii' - >>> mgr.get_description('it') - 'Italian' - >>> mgr.get_charset('it') - 'iso-8859-1' - -You can also add a language without enabling it. - - >>> mgr.add_language('pl', 'Polish', 'iso-8859-2', enable=False) - >>> sorted(mgr.known_codes) - ['en', 'it', 'pl'] - >>> sorted(mgr.enabled_codes) - ['en', 'it'] - -You can get language data for disabled languages. - - >>> mgr.get_description('pl') - 'Polish' - >>> mgr.get_charset('pl') - 'iso-8859-2' - -And of course you can enable a known language. - - >>> mgr.enable_language('pl') - >>> sorted(mgr.enabled_codes) - ['en', 'it', 'pl'] - -But you cannot enable languages that the manager does not know about. - - >>> mgr.enable_language('xx') - Traceback (most recent call last): - ... - KeyError: 'xx' - - -Other iterations ----------------- - -You can iterate over the descriptions (names) of all enabled languages. - - >>> sorted(mgr.enabled_names) - ['English', 'Italian', 'Polish'] - -You can ask whether a particular language code is enabled. - - >>> 'it' in mgr.enabled_codes - True diff --git a/mailman/docs/lifecycle.txt b/mailman/docs/lifecycle.txt deleted file mode 100644 index c6c0c0671..000000000 --- a/mailman/docs/lifecycle.txt +++ /dev/null @@ -1,136 +0,0 @@ -Application level list lifecycle --------------------------------- - -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) - - >>> from mailman.app.lifecycle import create_list - - -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): - ... - InvalidEmailAddress: '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 = u'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(u'test_1@example.com') - >>> mlist_1.fqdn_listname - u'test_1@example.com' - >>> mlist_1.msg_footer - u'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 = [u'aperson@example.com', u'bperson@example.com', - ... u'cperson@example.com', u'dperson@example.com'] - >>> mlist_2 = create_list(u'test_2@example.com', owners) - >>> mlist_2.fqdn_listname - u'test_2@example.com' - >>> mlist_2.msg_footer - u'test footer' - >>> sorted(addr.address for addr in mlist_2.owners.addresses) - [u'aperson@example.com', u'bperson@example.com', - u'cperson@example.com', u'dperson@example.com'] - -None of the owner addresses are verified. - - >>> any(addr.verified_on is not None for addr 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. - - >>> usermgr = config.db.user_manager - >>> user_a = usermgr.get_user(u'aperson@example.com') - >>> user_b = usermgr.get_user(u'bperson@example.com') - >>> user_c = usermgr.get_user(u'cperson@example.com') - >>> user_d = usermgr.get_user(u'dperson@example.com') - >>> user_a.real_name = u'Anne Person' - >>> user_b.real_name = u'Bart Person' - >>> user_c.real_name = u'Caty Person' - >>> user_d.real_name = u'Dirk Person' - - >>> mlist_3 = create_list(u'test_3@example.com', owners) - >>> sorted(user.real_name for user in mlist_3.owners.users) - [u'Anne Person', u'Bart Person', u'Caty Person', u'Dirk Person'] - - -Removing 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) - >>> print config.db.list_manager.get('test_2@example.com') - None - -We should now be able to completely recreate the mailing list. - - >>> mlist_2a = create_list(u'test_2@example.com', owners) - >>> sorted(addr.address for addr in mlist_2a.owners.addresses) - [u'aperson@example.com', u'bperson@example.com', - u'cperson@example.com', u'dperson@example.com'] diff --git a/mailman/docs/listmanager.txt b/mailman/docs/listmanager.txt deleted file mode 100644 index 830f6d962..000000000 --- a/mailman/docs/listmanager.txt +++ /dev/null @@ -1,88 +0,0 @@ -Using the IListManager interface -================================ - -The IListManager is how you create, delete, and retrieve mailing list -objects. The Mailman system instantiates an IListManager for you based on the -configuration variable MANAGERS_INIT_FUNCTION. The instance is accessible -on the global config object. - - >>> from mailman.interfaces.listmanager import IListManager - >>> listmgr = config.db.list_manager - >>> IListManager.providedBy(listmgr) - True - - -Creating a mailing list ------------------------ - -Creating the list returns the newly created IMailList object. - - >>> from mailman.interfaces.mailinglist import IMailingList - >>> mlist = listmgr.create(u'_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. - - >>> mlist.list_name - u'_xtest' - >>> mlist.host_name - u'example.com' - >>> mlist.fqdn_listname - u'_xtest@example.com' - -If you try to create a mailing list with the same name as an existing list, -you will get an exception. - - >>> mlist_dup = listmgr.create(u'_xtest@example.com') - Traceback (most recent call last): - ... - ListAlreadyExistsError: _xtest@example.com - - -Deleting a mailing list ------------------------ - -Use the list manager to delete a mailing list. - - >>> listmgr.delete(mlist) - >>> sorted(listmgr.names) - [] - -After deleting the list, you can create it again. - - >>> mlist = listmgr.create(u'_xtest@example.com') - >>> mlist.fqdn_listname - u'_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 = listmgr.get(u'_xtest@example.com') - >>> mlist_2 is mlist - True - -If you try to get a list that doesn't existing yet, you get None. - - >>> print listmgr.get(u'_xtest_2@example.com') - 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 = listmgr.create(u'_xtest_3@example.com') - >>> mlist_4 = listmgr.create(u'_xtest_4@example.com') - >>> sorted(listmgr.names) - [u'_xtest@example.com', u'_xtest_3@example.com', u'_xtest_4@example.com'] - >>> sorted(m.fqdn_listname for m in listmgr.mailing_lists) - [u'_xtest@example.com', u'_xtest_3@example.com', u'_xtest_4@example.com'] diff --git a/mailman/docs/membership.txt b/mailman/docs/membership.txt deleted file mode 100644 index 7f9f16738..000000000 --- a/mailman/docs/membership.txt +++ /dev/null @@ -1,230 +0,0 @@ -List memberships -================ - -Users represent people in Mailman. Users control email addresses, and rosters -are collectons of members. A member gives an email address a role, such as -'member', 'administrator', or 'moderator'. Roster sets are collections of -rosters and a mailing list has a single roster set that contains all its -members, regardless of that member's role. - -Mailing lists and roster sets have an indirect relationship, through the -roster set's name. Roster also have names, but are related to roster sets -by a more direct containment relationship. This is because it is possible to -store mailing list data in a different database than user data. - -When we create a mailing list, it starts out with no members... - - >>> mlist = config.db.list_manager.create(u'_xtest@example.com') - >>> mlist - - >>> sorted(member.address.address for member in mlist.members.members) - [] - >>> sorted(user.real_name for user in mlist.members.users) - [] - >>> sorted(address.address for member in mlist.members.addresses) - [] - -...no owners... - - >>> sorted(member.address.address for member in mlist.owners.members) - [] - >>> sorted(user.real_name for user in mlist.owners.users) - [] - >>> sorted(address.address for member in mlist.owners.addresses) - [] - -...no moderators... - - >>> sorted(member.address.address for member in mlist.moderators.members) - [] - >>> sorted(user.real_name for user in mlist.moderators.users) - [] - >>> sorted(address.address for member in mlist.moderators.addresses) - [] - -...and no administrators. - - >>> sorted(member.address.address - ... for member in mlist.administrators.members) - [] - >>> sorted(user.real_name for user in mlist.administrators.users) - [] - >>> sorted(address.address for member in mlist.administrators.addresses) - [] - - - -Administrators --------------- - -A mailing list's administrators are defined as union of the list's owners and -the list's moderators. We can add new owners or moderators to this list by -assigning roles to users. First we have to create the user, because there are -no users in the user database yet. - - >>> usermgr = config.db.user_manager - >>> user_1 = usermgr.create_user(u'aperson@example.com', u'Anne Person') - >>> user_1.real_name - u'Anne Person' - >>> sorted(address.address for address in user_1.addresses) - [u'aperson@example.com'] - -We can add Anne as an owner of the mailing list, by creating a member role for -her. - - >>> from mailman.interfaces.member import MemberRole - >>> address_1 = list(user_1.addresses)[0] - >>> address_1.address - u'aperson@example.com' - >>> address_1.subscribe(mlist, MemberRole.owner) - on - _xtest@example.com as MemberRole.owner> - >>> sorted(member.address.address for member in mlist.owners.members) - [u'aperson@example.com'] - >>> sorted(user.real_name for user in mlist.owners.users) - [u'Anne Person'] - >>> sorted(address.address for address in mlist.owners.addresses) - [u'aperson@example.com'] - -Adding Anne as a list owner also makes her an administrator, but does not make -her a moderator. Nor does it make her a member of the list. - - >>> sorted(user.real_name for user in mlist.administrators.users) - [u'Anne Person'] - >>> sorted(user.real_name for user in mlist.moderators.users) - [] - >>> sorted(user.real_name for user in mlist.members.users) - [] - -We can add Ben as a moderator of the list, by creating a different member role -for him. - - >>> user_2 = usermgr.create_user(u'bperson@example.com', u'Ben Person') - >>> user_2.real_name - u'Ben Person' - >>> address_2 = list(user_2.addresses)[0] - >>> address_2.address - u'bperson@example.com' - >>> address_2.subscribe(mlist, MemberRole.moderator) - - on _xtest@example.com as MemberRole.moderator> - >>> sorted(member.address.address for member in mlist.moderators.members) - [u'bperson@example.com'] - >>> sorted(user.real_name for user in mlist.moderators.users) - [u'Ben Person'] - >>> sorted(address.address for address in mlist.moderators.addresses) - [u'bperson@example.com'] - -Now, both Anne and Ben are list administrators. - - >>> sorted(member.address.address - ... for member in mlist.administrators.members) - [u'aperson@example.com', u'bperson@example.com'] - >>> sorted(user.real_name for user in mlist.administrators.users) - [u'Anne Person', u'Ben Person'] - >>> sorted(address.address for address in mlist.administrators.addresses) - [u'aperson@example.com', u'bperson@example.com'] - - -Members -------- - -Similarly, list members are born of users being given the proper role. It's -more interesting here because these roles should have a preference which can -be used to decide whether the member is to get regular delivery or digest -delivery. Without a preference, Mailman will fall back first to the address's -preference, then the user's preference, then the list's preference. Start -without any member preference to see the system defaults. - - >>> user_3 = usermgr.create_user(u'cperson@example.com', u'Claire Person') - >>> user_3.real_name - u'Claire Person' - >>> address_3 = list(user_3.addresses)[0] - >>> address_3.address - u'cperson@example.com' - >>> address_3.subscribe(mlist, MemberRole.member) - - on _xtest@example.com as MemberRole.member> - -Claire will be a regular delivery member but not a digest member. - - >>> sorted(address.address for address in mlist.members.addresses) - [u'cperson@example.com'] - >>> sorted(address.address for address in mlist.regular_members.addresses) - [u'cperson@example.com'] - >>> sorted(address.address for address in mlist.digest_members.addresses) - [] - -It's easy to make the list administrators members of the mailing list too. - - >>> members = [] - >>> for address in mlist.administrators.addresses: - ... member = address.subscribe(mlist, MemberRole.member) - ... members.append(member) - >>> sorted(members, key=lambda m: m.address.address) - [ on - _xtest@example.com as MemberRole.member>, - on - _xtest@example.com as MemberRole.member>] - >>> sorted(address.address for address in mlist.members.addresses) - [u'aperson@example.com', u'bperson@example.com', u'cperson@example.com'] - >>> sorted(address.address for address in mlist.regular_members.addresses) - [u'aperson@example.com', u'bperson@example.com', u'cperson@example.com'] - >>> sorted(address.address for address in mlist.digest_members.addresses) - [] - - -Finding members ---------------- - -You can find the IMember object that is a member of a roster for a given text -email address by using an IRoster's .get_member() method. - - >>> mlist.owners.get_member(u'aperson@example.com') - on - _xtest@example.com as MemberRole.owner> - >>> mlist.administrators.get_member(u'aperson@example.com') - on - _xtest@example.com as MemberRole.owner> - >>> mlist.members.get_member(u'aperson@example.com') - on - _xtest@example.com as MemberRole.member> - -However, if the address is not subscribed with the appropriate role, then None -is returned. - - >>> print mlist.administrators.get_member(u'zperson@example.com') - None - >>> print mlist.moderators.get_member(u'aperson@example.com') - None - >>> print mlist.members.get_member(u'zperson@example.com') - None - - -All subscribers ---------------- - -There is also a roster containing all the subscribers of a mailing list, -regardless of their role. - - >>> def sortkey(member): - ... return (member.address.address, int(member.role)) - >>> [(member.address.address, str(member.role)) - ... for member in sorted(mlist.subscribers.members, key=sortkey)] - [(u'aperson@example.com', 'MemberRole.member'), - (u'aperson@example.com', 'MemberRole.owner'), - (u'bperson@example.com', 'MemberRole.member'), - (u'bperson@example.com', 'MemberRole.moderator'), - (u'cperson@example.com', 'MemberRole.member')] - - -Double subscriptions --------------------- - -It is an error to subscribe someone to a list with the same role twice. - - >>> address_1.subscribe(mlist, MemberRole.owner) - Traceback (most recent call last): - ... - AlreadySubscribedError: aperson@example.com is already a MemberRole.owner - of mailing list _xtest@example.com diff --git a/mailman/docs/message.txt b/mailman/docs/message.txt deleted file mode 100644 index dab9ddf0e..000000000 --- a/mailman/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 = config.db.list_manager.create(u'_xtest@example.com') - >>> mlist.preferred_language = u'en' - -The UserNotification constructor takes the recipient address, the sender -address, an optional subject, optional body text, and optional language. - - >>> from mailman.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/mailman/docs/messagestore.txt b/mailman/docs/messagestore.txt deleted file mode 100644 index 6e04568c5..000000000 --- a/mailman/docs/messagestore.txt +++ /dev/null @@ -1,113 +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. - - >>> store = config.db.message_store - -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. - ... """) - >>> 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>' - >>> 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 store.get_message_by_id(u'nothing') - None - >>> print store.get_message_by_hash(u'nothing') - None - -Given an existing Message-ID, the message can be found. - - >>> 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 = 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(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. - - >>> store.delete_message(u'nothing') - Traceback (most recent call last): - ... - LookupError: nothing - -But if you delete an existing message, it really gets deleted. - - >>> message_id = message['message-id'] - >>> store.delete_message(message_id) - >>> list(store.messages) - [] - >>> print store.get_message_by_id(message_id) - None - >>> print store.get_message_by_hash(message['x-message-id-hash']) - None diff --git a/mailman/docs/mlist-addresses.txt b/mailman/docs/mlist-addresses.txt deleted file mode 100644 index 75ec3df37..000000000 --- a/mailman/docs/mlist-addresses.txt +++ /dev/null @@ -1,76 +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 = config.db.list_manager.create(u'_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. - - >>> mlist.fqdn_listname - u'_xtest@example.com' - >>> mlist.posting_address - u'_xtest@example.com' - -Messages to the mailing list's 'no reply' address always get discarded without -prejudice. - - >>> mlist.no_reply_address - u'noreply@example.com' - -The mailing list's owner address reaches the human moderators. - - >>> mlist.owner_address - u'_xtest-owner@example.com' - -The request address goes to the list's email command robot. - - >>> mlist.request_address - u'_xtest-request@example.com' - -The bounces address accepts and processes all potential bounces. - - >>> mlist.bounces_address - u'_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. - - >>> mlist.join_address - u'_xtest-join@example.com' - >>> mlist.subscribe_address - u'_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. - - >>> mlist.leave_address - u'_xtest-leave@example.com' - >>> mlist.unsubscribe_address - u'_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. - - >>> mlist.confirm_address('cookie') - u'_xtest-confirm+cookie@example.com' - >>> mlist.confirm_address('wookie') - u'_xtest-confirm+wookie@example.com' - - >>> config.push('test config', """ - ... [mta] - ... verp_confirm_format: $address---$cookie - ... """) - >>> mlist.confirm_address('cookie') - u'_xtest-confirm---cookie@example.com' - >>> config.pop('test config') diff --git a/mailman/docs/pending.txt b/mailman/docs/pending.txt deleted file mode 100644 index abfba4885..000000000 --- a/mailman/docs/pending.txt +++ /dev/null @@ -1,94 +0,0 @@ -The pending database -==================== - -The pending database is where various types of events which need confirmation -are stored. These can include email address registration events, held -messages (but only for user confirmation), auto-approvals, and probe bounces. -This is not where messages held for administrator approval are kept. - - >>> from zope.interface import implements - >>> from zope.interface.verify import verifyObject - -In order to pend an event, you first need a pending database, which is -available by adapting the list manager. - - >>> from mailman.interfaces.pending import IPendings - >>> pendingdb = config.db.pendings - >>> verifyObject(IPendings, pendingdb) - True - -The pending database can add any IPendable to the database, returning a token -that can be used in urls and such. - - >>> from mailman.interfaces.pending import IPendable - >>> class SimplePendable(dict): - ... implements(IPendable) - >>> subscription = SimplePendable( - ... type='subscription', - ... address='aperson@example.com', - ... realname='Anne Person', - ... language='en', - ... password='xyz') - >>> token = pendingdb.add(subscription) - >>> len(token) - 40 - -There's not much you can do with tokens except to 'confirm' them, which -basically means returning the IPendable structure (as a dict) from the -database that matches the token. If the token isn't in the database, None is -returned. - - >>> pendable = pendingdb.confirm('missing') - >>> print pendable - None - >>> pendable = pendingdb.confirm(token) - >>> sorted(pendable.items()) - [(u'address', u'aperson@example.com'), - (u'language', u'en'), - (u'password', u'xyz'), - (u'realname', u'Anne Person'), - (u'type', u'subscription')] - -After confirmation, the token is no longer in the database. - - >>> pendable = pendingdb.confirm(token) - >>> print pendable - None - -There are a few other things you can do with the pending database. When you -confirm a token, you can leave it in the database, or in otherwords, not -expunge it. - - >>> event_1 = SimplePendable(type='one') - >>> token_1 = pendingdb.add(event_1) - >>> event_2 = SimplePendable(type='two') - >>> token_2 = pendingdb.add(event_2) - >>> event_3 = SimplePendable(type='three') - >>> token_3 = pendingdb.add(event_3) - >>> pendable = pendingdb.confirm(token_1, expunge=False) - >>> pendable.items() - [(u'type', u'one')] - >>> pendable = pendingdb.confirm(token_1, expunge=True) - >>> pendable.items() - [(u'type', u'one')] - >>> pendable = pendingdb.confirm(token_1) - >>> print pendable - None - -An event can be given a lifetime when it is pended, otherwise it just uses a -default lifetime. - - >>> from datetime import timedelta - >>> yesterday = timedelta(days=-1) - >>> event_4 = SimplePendable(type='four') - >>> token_4 = pendingdb.add(event_4, lifetime=yesterday) - -Every once in a while the pending database is cleared of old records. - - >>> pendingdb.evict() - >>> pendable = pendingdb.confirm(token_4) - >>> print pendable - None - >>> pendable = pendingdb.confirm(token_2) - >>> pendable.items() - [(u'type', u'two')] diff --git a/mailman/docs/pipelines.txt b/mailman/docs/pipelines.txt deleted file mode 100644 index 0e6dad8e8..000000000 --- a/mailman/docs/pipelines.txt +++ /dev/null @@ -1,186 +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. - - >>> from mailman.app.lifecycle import create_list - >>> mlist = create_list(u'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 - recips : 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 - recips : 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 - recips : 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! - - - - >>> digest.clear() diff --git a/mailman/docs/registration.txt b/mailman/docs/registration.txt deleted file mode 100644 index d243188bc..000000000 --- a/mailman/docs/registration.txt +++ /dev/null @@ -1,362 +0,0 @@ -Address registration -==================== - -When a user wants to join a mailing list -- any mailing list -- in the running -instance, he or she 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. - - >>> from mailman.app.registrar import Registrar - >>> from mailman.interfaces.registrar import IRegistrar - -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. - -Add a domain, which will provide the context for the verification email -message. - - >>> config.push('mail', """ - ... [domain.mail_example_dot_com] - ... email_host: mail.example.com - ... base_url: http://mail.example.com - ... contact_address: postmaster@mail.example.com - ... """) - - >>> domain = config.domains['mail.example.com'] - -Get a registrar by adapting a context to the interface. - - >>> from zope.interface.verify import verifyObject - >>> registrar = IRegistrar(domain) - >>> verifyObject(IRegistrar, registrar) - True - -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://mail.example.com/confirm/(.*)') - >>> def extract_token(msg): - ... mo = cre.search(qmsg.get_payload()) - ... return mo.group(1) - - -Invalid email addresses ------------------------ - -The only piece of information you need to register is the email address. -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('') - Traceback (most recent call last): - ... - InvalidEmailAddress: '' - >>> registrar.register('some name@example.com') - Traceback (most recent call last): - ... - InvalidEmailAddress: 'some name@example.com' - >>> registrar.register(' tags but nothing else. Not the best - # solution, but expedient. - return re.sub(r'<([/]?script.*?)>', r'<\1>', value) - - def _postValidate(self, mlist, doc): - if not mlist.reply_to_address.strip() and \ - mlist.reply_goes_to_list == 2: - # You can't go to an explicit address that is blank - doc.addError(_("""You cannot add a Reply-To: to an explicit - address if that address is blank. Resetting these values.""")) - mlist.reply_to_address = '' - mlist.reply_goes_to_list = 0 - - def getValue(self, mlist, kind, varname, params): - if varname <> 'subject_prefix': - return None - # The subject_prefix may be Unicode - return Utils.uncanonstr(mlist.subject_prefix, mlist.preferred_language) diff --git a/mailman/web/Gui/Language.py b/mailman/web/Gui/Language.py deleted file mode 100644 index 05824be5e..000000000 --- a/mailman/web/Gui/Language.py +++ /dev/null @@ -1,128 +0,0 @@ -# Copyright (C) 2001-2009 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 . - -"""MailList mixin class managing the language options.""" - -import codecs - -from Mailman import Utils -from Mailman import i18n -from Mailman.Gui.GUIBase import GUIBase -from Mailman.configuration import config - -_ = i18n._ - - - -class Language(GUIBase): - def GetConfigCategory(self): - return 'language', _('Language options') - - def GetConfigInfo(self, mlist, category, subcat=None): - if category <> 'language': - return None - # Set things up for the language choices - langs = mlist.language_codes - langnames = [_(description) for description in config.enabled_names] - try: - langi = langs.index(mlist.preferred_language) - except ValueError: - # Someone must have deleted the list's preferred language. Could - # be other trouble lurking! - langi = 0 - # Only allow the admin to choose a language if the system has a - # charset for it. I think this is the best way to test for that. - def checkcodec(charset): - try: - codecs.lookup(charset) - return 1 - except LookupError: - return 0 - - all = sorted(code for code in config.languages.enabled_codes - if checkcodec(Utils.GetCharSet(code))) - checked = [L in langs for L in all] - allnames = [_(config.languages.get_description(code)) for code in all] - return [ - _('Natural language (internationalization) options.'), - - ('preferred_language', config.Select, - (langs, langnames, langi), - 0, - _('Default language for this list.'), - _('''This is the default natural language for this mailing list. - If more than one - language is supported then users will be able to select their - own preferences for when they interact with the list. All other - interactions will be conducted in the default language. This - applies to both web-based and email-based messages, but not to - email posted by list members.''')), - - ('available_languages', config.Checkbox, - (allnames, checked, 0, all), 0, - _('Languages supported by this list.'), - - _('''These are all the natural languages supported by this list. - Note that the - default - language must be included.''')), - - ('encode_ascii_prefixes', config.Radio, - (_('Never'), _('Always'), _('As needed')), 0, - _("""Encode the - subject - prefix even when it consists of only ASCII characters?"""), - - _("""If your mailing list's default language uses a non-ASCII - character set and the prefix contains non-ASCII characters, the - prefix will always be encoded according to the relevant - standards. However, if your prefix contains only ASCII - characters, you may want to set this option to Never to - disable prefix encoding. This can make the subject headers - slightly more readable for users with mail readers that don't - properly handle non-ASCII encodings. - -

      Note however, that if your mailing list receives both encoded - and unencoded subject headers, you might want to choose As - needed. Using this setting, Mailman will not encode ASCII - prefixes when the rest of the header contains only ASCII - characters, but if the original header contains non-ASCII - characters, it will encode the prefix. This avoids an ambiguity - in the standards which could cause some mail readers to display - extra, or missing spaces between the prefix and the original - header.""")), - - ] - - def _setValue(self, mlist, prop, val, doc): - # If we're changing the list's preferred language, change the I18N - # context as well - if prop == 'preferred_language': - i18n.set_language(val) - doc.set_language(val) - # Language codes must be wrapped - if prop == 'available_languages': - mlist.set_languages(*val) - else: - GUIBase._setValue(self, mlist, prop, val, doc) - - def getValue(self, mlist, kind, varname, params): - if varname == 'available_languages': - # Unwrap Language instances, to return just the code - return [language.code for language in mlist.available_languages] - # Returning None tells the infrastructure to use getattr - return None diff --git a/mailman/web/Gui/Membership.py b/mailman/web/Gui/Membership.py deleted file mode 100644 index bbfdb438b..000000000 --- a/mailman/web/Gui/Membership.py +++ /dev/null @@ -1,34 +0,0 @@ -# Copyright (C) 2001-2009 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 . - -"""MailList mixin class managing the membership pseudo-options.""" - -from Mailman.i18n import _ - - - -class Membership: - def GetConfigCategory(self): - return 'members', _('Membership Management...') - - def GetConfigSubCategories(self, category): - if category == 'members': - return [('list', _('Membership List')), - ('add', _('Mass Subscription')), - ('remove', _('Mass Removal')), - ] - return None diff --git a/mailman/web/Gui/NonDigest.py b/mailman/web/Gui/NonDigest.py deleted file mode 100644 index 92fb768ad..000000000 --- a/mailman/web/Gui/NonDigest.py +++ /dev/null @@ -1,158 +0,0 @@ -# Copyright (C) 2001-2009 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 . - -"""GUI component for managing the non-digest delivery options.""" - -from Mailman import Utils -from Mailman import Defaults -from Mailman.i18n import _ -from Mailman.configuration import config -from Mailman.Gui.GUIBase import GUIBase - -from Mailman.Gui.Digest import ALLOWEDS -PERSONALIZED_ALLOWEDS = ('user_address', 'user_delivered_to', 'user_password', - 'user_name', 'user_optionsurl', - ) - - - -class NonDigest(GUIBase): - def GetConfigCategory(self): - return 'nondigest', _('Non-digest options') - - def GetConfigInfo(self, mlist, category, subcat=None): - if category <> 'nondigest': - return None - WIDTH = config.TEXTFIELDWIDTH - - info = [ - _("Policies concerning immediately delivered list traffic."), - - ('nondigestable', Defaults.Toggle, (_('No'), _('Yes')), 1, - _("""Can subscribers choose to receive mail immediately, rather - than in batched digests?""")), - ] - - if config.OWNERS_CAN_ENABLE_PERSONALIZATION: - info.extend([ - ('personalize', Defaults.Radio, - (_('No'), _('Yes'), _('Full Personalization')), 1, - - _('''Should Mailman personalize each non-digest delivery? - This is often useful for announce-only lists, but read the details - section for a discussion of important performance - issues.'''), - - _("""Normally, Mailman sends the regular delivery messages to - the mail server in batches. This is much more efficent - because it reduces the amount of traffic between Mailman and - the mail server. - -

      However, some lists can benefit from a more personalized - approach. In this case, Mailman crafts a new message for - each member on the regular delivery list. Turning this - feature on may degrade the performance of your site, so you - need to carefully consider whether the trade-off is worth it, - or whether there are other ways to accomplish what you want. - You should also carefully monitor your system load to make - sure it is acceptable. - -

      Select No to disable personalization and send - messages to the members in batches. Select Yes to - personalize deliveries and allow additional substitution - variables in message headers and footers (see below). In - addition, by selecting Full Personalization, the - To header of posted messages will be modified to - include the member's address instead of the list's posting - address. - -

      When personalization is enabled, a few more expansion - variables that can be included in the message header and - message footer. - -

      These additional substitution variables will be available - for your headers and footers, when this feature is enabled: - -

      • user_address - The address of the user, - coerced to lower case. -
      • user_delivered_to - The case-preserved address - that the user is subscribed with. -
      • user_password - The user's password. -
      • user_name - The user's full name. -
      • user_optionsurl - The url to the user's option - page. -
      - """)) - ]) - # BAW: for very dumb reasons, we want the `personalize' attribute to - # show up before the msg_header and msg_footer attrs, otherwise we'll - # get a bogus warning if the header/footer contains a personalization - # substitution variable, and we're transitioning from no - # personalization to personalization enabled. - headfoot = Utils.maketext('headfoot.html', mlist=mlist, raw=1) - if config.OWNERS_CAN_ENABLE_PERSONALIZATION: - extra = _("""\ -When personalization is enabled -for this list, additional substitution variables are allowed in your headers -and footers: - -
      • user_address - The address of the user, - coerced to lower case. -
      • user_delivered_to - The case-preserved address - that the user is subscribed with. -
      • user_password - The user's password. -
      • user_name - The user's full name. -
      • user_optionsurl - The url to the user's option - page. -
      -""") - else: - extra = '' - - info.extend([('msg_header', Defaults.Text, (10, WIDTH), 0, - _('Header added to mail sent to regular list members'), - _('''Text prepended to the top of every immediately-delivery - message. ''') + headfoot + extra), - - ('msg_footer', Defaults.Text, (10, WIDTH), 0, - _('Footer added to mail sent to regular list members'), - _('''Text appended to the bottom of every immediately-delivery - message. ''') + headfoot + extra), - ]) - - info.extend([ - ('scrub_nondigest', Defaults.Toggle, (_('No'), _('Yes')), 0, - _('Scrub attachments of regular delivery message?'), - _('''When you scrub attachments, they are stored in archive - area and links are made in the message so that the member can - access via web browser. If you want the attachments totally - disappear, you can use content filter options.''')), - ]) - return info - - def _setValue(self, mlist, property, val, doc): - alloweds = list(ALLOWEDS) - if mlist.personalize: - alloweds.extend(PERSONALIZED_ALLOWEDS) - if property in ('msg_header', 'msg_footer'): - val = self._convertString(mlist, property, alloweds, val, doc) - if val is None: - # There was a problem, so don't set it - return - GUIBase._setValue(self, mlist, property, val, doc) diff --git a/mailman/web/Gui/Passwords.py b/mailman/web/Gui/Passwords.py deleted file mode 100644 index b2fea0fb5..000000000 --- a/mailman/web/Gui/Passwords.py +++ /dev/null @@ -1,31 +0,0 @@ -# Copyright (C) 2001-2009 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 . - -"""MailList mixin class managing the password pseudo-options.""" - -from Mailman.i18n import _ -from Mailman.Gui.GUIBase import GUIBase - - - -class Passwords(GUIBase): - def GetConfigCategory(self): - return 'passwords', _('Passwords') - - def handleForm(self, mlist, category, subcat, cgidata, doc): - # Nothing more needs to be done - pass diff --git a/mailman/web/Gui/Privacy.py b/mailman/web/Gui/Privacy.py deleted file mode 100644 index 8d60f9203..000000000 --- a/mailman/web/Gui/Privacy.py +++ /dev/null @@ -1,537 +0,0 @@ -# Copyright (C) 2001-2009 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 . - -"""MailList mixin class managing the privacy options.""" - -import re - -from Mailman import Utils -from Mailman.Gui.GUIBase import GUIBase -from Mailman.configuration import config -from Mailman.i18n import _ - - - -class Privacy(GUIBase): - def GetConfigCategory(self): - return 'privacy', _('Privacy options...') - - def GetConfigSubCategories(self, category): - if category == 'privacy': - return [('subscribing', _('Subscription rules')), - ('sender', _('Sender filters')), - ('recipient', _('Recipient filters')), - ('spam', _('Spam filters')), - ] - return None - - def GetConfigInfo(self, mlist, category, subcat=None): - if category <> 'privacy': - return None - # Pre-calculate some stuff. Technically, we shouldn't do the - # sub_cfentry calculation here, but it's too ugly to indent it any - # further, and besides, that'll mess up i18n catalogs. - WIDTH = config.TEXTFIELDWIDTH - if config.ALLOW_OPEN_SUBSCRIBE: - sub_cfentry = ('subscribe_policy', config.Radio, - # choices - (_('None'), - _('Confirm'), - _('Require approval'), - _('Confirm and approve')), - 0, - _('What steps are required for subscription?
      '), - _('''None - no verification steps (Not - Recommended )
      - Confirm (*) - email confirmation step required
      - Require approval - require list administrator - Approval for subscriptions
      - Confirm and approve - both confirm and approve - -

      (*) when someone requests a subscription, - Mailman sends them a notice with a unique - subscription request number that they must reply to - in order to subscribe.
      - - This prevents mischievous (or malicious) people - from creating subscriptions for others without - their consent.''')) - else: - sub_cfentry = ('subscribe_policy', config.Radio, - # choices - (_('Confirm'), - _('Require approval'), - _('Confirm and approve')), - 1, - _('What steps are required for subscription?
      '), - _('''Confirm (*) - email confirmation required
      - Require approval - require list administrator - approval for subscriptions
      - Confirm and approve - both confirm and approve - -

      (*) when someone requests a subscription, - Mailman sends them a notice with a unique - subscription request number that they must reply to - in order to subscribe.
      This prevents - mischievous (or malicious) people from creating - subscriptions for others without their consent.''')) - - # some helpful values - admin = mlist.GetScriptURL('admin') - - subscribing_rtn = [ - _("""This section allows you to configure subscription and - membership exposure policy. You can also control whether this - list is public or not. See also the - Archival Options section for - separate archive-related privacy settings."""), - - _('Subscribing'), - ('advertised', config.Radio, (_('No'), _('Yes')), 0, - _('''Advertise this list when people ask what lists are on this - machine?''')), - - sub_cfentry, - - ('subscribe_auto_approval', config.EmailListEx, (10, WIDTH), 1, - _("""List of addresses (or regexps) whose subscriptions do not - require approval."""), - - _("""When subscription requires approval, addresses in this list - are allowed to subscribe without administrator approval. Add - addresses one per line. You may begin a line with a ^ character - to designate a (case insensitive) regular expression match.""")), - - ('unsubscribe_policy', config.Radio, (_('No'), _('Yes')), 0, - _("""Is the list moderator's approval required for unsubscription - requests? (No is recommended)"""), - - _("""When members want to leave a list, they will make an - unsubscription request, either via the web or via email. - Normally it is best for you to allow open unsubscriptions so that - users can easily remove themselves from mailing lists (they get - really upset if they can't get off lists!). - -

      For some lists though, you may want to impose moderator - approval before an unsubscription request is processed. Examples - of such lists include a corporate mailing list that all employees - are required to be members of.""")), - - _('Ban list'), - ('ban_list', config.EmailListEx, (10, WIDTH), 1, - _("""List of addresses which are banned from membership in this - mailing list."""), - - _("""Addresses in this list are banned outright from subscribing - to this mailing list, with no further moderation required. Add - addresses one per line; start the line with a ^ character to - designate a regular expression match.""")), - - _("Membership exposure"), - ('private_roster', config.Radio, - (_('Anyone'), _('List members'), _('List admin only')), 0, - _('Who can view subscription list?'), - - _('''When set, the list of subscribers is protected by member or - admin password authentication.''')), - - ('obscure_addresses', config.Radio, (_('No'), _('Yes')), 0, - _("""Show member addresses so they're not directly recognizable - as email addresses?"""), - _("""Setting this option causes member email addresses to be - transformed when they are presented on list web pages (both in - text and as links), so they're not trivially recognizable as - email addresses. The intention is to prevent the addresses - from being snarfed up by automated web scanners for use by - spammers.""")), - ] - - adminurl = mlist.GetScriptURL('admin') - sender_rtn = [ - _("""When a message is posted to the list, a series of - moderation steps are take to decide whether the a moderator must - first approve the message or not. This section contains the - controls for moderation of both member and non-member postings. - -

      Member postings are held for moderation if their - moderation flag is turned on. You can control whether - member postings are moderated by default or not. - -

      Non-member postings can be automatically - accepted, - held for - moderation, - rejected (bounced), or - discarded, - either individually or as a group. Any - posting from a non-member who is not explicitly accepted, - rejected, or discarded, will have their posting filtered by the - general - non-member rules. - -

      In the text boxes below, add one address per line; start the - line with a ^ character to designate a Python regular expression. When entering backslashes, do so - as if you were using Python raw strings (i.e. you generally just - use a single backslash). - -

      Note that non-regexp matches are always done first."""), - - _('Member filters'), - - ('default_member_moderation', config.Radio, (_('No'), _('Yes')), - 0, _('By default, should new list member postings be moderated?'), - - _("""Each list member has a moderation flag which says - whether messages from the list member can be posted directly to - the list, or must first be approved by the list moderator. When - the moderation flag is turned on, list member postings must be - approved first. You, the list administrator can decide whether a - specific individual's postings will be moderated or not. - -

      When a new member is subscribed, their initial moderation flag - takes its value from this option. Turn this option off to accept - member postings by default. Turn this option on to, by default, - moderate member postings first. You can always manually set an - individual member's moderation bit by using the - membership management - screens.""")), - - ('member_moderation_action', config.Radio, - (_('Hold'), _('Reject'), _('Discard')), 0, - _("""Action to take when a moderated member posts to the - list."""), - _("""

      • Hold -- this holds the message for approval - by the list moderators. - -

      • Reject -- this automatically rejects the message by - sending a bounce notice to the post's author. The text of the - bounce notice can be configured by you. - -

      • Discard -- this simply discards the message, with - no notice sent to the post's author. -
      """)), - - ('member_moderation_notice', config.Text, (10, WIDTH), 1, - _("""Text to include in any - rejection notice to - be sent to moderated members who post to this list.""")), - - _('Non-member filters'), - - ('accept_these_nonmembers', config.EmailListEx, (10, WIDTH), 1, - _("""List of non-member addresses whose postings should be - automatically accepted."""), - - _("""Postings from any of these non-members will be automatically - accepted with no further moderation applied. Add member - addresses one per line; start the line with a ^ character to - designate a regular expression match.""")), - - ('hold_these_nonmembers', config.EmailListEx, (10, WIDTH), 1, - _("""List of non-member addresses whose postings will be - immediately held for moderation."""), - - _("""Postings from any of these non-members will be immediately - and automatically held for moderation by the list moderators. - The sender will receive a notification message which will allow - them to cancel their held message. Add member addresses one per - line; start the line with a ^ character to designate a regular - expression match.""")), - - ('reject_these_nonmembers', config.EmailListEx, (10, WIDTH), 1, - _("""List of non-member addresses whose postings will be - automatically rejected."""), - - _("""Postings from any of these non-members will be automatically - rejected. In other words, their messages will be bounced back to - the sender with a notification of automatic rejection. This - option is not appropriate for known spam senders; their messages - should be - automatically discarded. - -

      Add member addresses one per line; start the line with a ^ - character to designate a regular expression match.""")), - - ('discard_these_nonmembers', config.EmailListEx, (10, WIDTH), 1, - _("""List of non-member addresses whose postings will be - automatically discarded."""), - - _("""Postings from any of these non-members will be automatically - discarded. That is, the message will be thrown away with no - further processing or notification. The sender will not receive - a notification or a bounce, however the list moderators can - optionally receive copies of auto-discarded messages.. - -

      Add member addresses one per line; start the line with a ^ - character to designate a regular expression match.""")), - - ('generic_nonmember_action', config.Radio, - (_('Accept'), _('Hold'), _('Reject'), _('Discard')), 0, - _("""Action to take for postings from non-members for which no - explicit action is defined."""), - - _("""When a post from a non-member is received, the message's - sender is matched against the list of explicitly - accepted, - held, - rejected (bounced), and - discarded addresses. If no match is found, then this action - is taken.""")), - - ('forward_auto_discards', config.Radio, (_('No'), _('Yes')), 0, - _("""Should messages from non-members, which are automatically - discarded, be forwarded to the list moderator?""")), - - ('nonmember_rejection_notice', config.Text, (10, WIDTH), 1, - _("""Text to include in any rejection notice to be sent to - non-members who post to this list. This notice can include - the list's owner address by %%(listowner)s and replaces the - internally crafted default message.""")), - - ] - - recip_rtn = [ - _("""This section allows you to configure various filters based on - the recipient of the message."""), - - _('Recipient filters'), - - ('require_explicit_destination', config.Radio, - (_('No'), _('Yes')), 0, - _("""Must posts have list named in destination (to, cc) field - (or be among the acceptable alias names, specified below)?"""), - - _("""Many (in fact, most) spams do not explicitly name their - myriad destinations in the explicit destination addresses - in - fact often the To: field has a totally bogus address for - obfuscation. The constraint applies only to the stuff in the - address before the '@' sign, but still catches all such spams. - -

      The cost is that the list will not accept unhindered any - postings relayed from other addresses, unless - -

        -
      1. The relaying address has the same name, or - -
      2. The relaying address name is included on the options that - specifies acceptable aliases for the list. - -
      """)), - - ('acceptable_aliases', config.Text, (4, WIDTH), 0, - _("""Alias names (regexps) which qualify as explicit to or cc - destination names for this list."""), - - _("""Alternate addresses that are acceptable when - `require_explicit_destination' is enabled. This option takes a - list of regular expressions, one per line, which is matched - against every recipient address in the message. The matching is - performed with Python's re.match() function, meaning they are - anchored to the start of the string. - -

      For backwards compatibility with Mailman 1.1, if the regexp - does not contain an `@', then the pattern is matched against just - the local part of the recipient address. If that match fails, or - if the pattern does contain an `@', then the pattern is matched - against the entire recipient address. - -

      Matching against the local part is deprecated; in a future - release, the pattern will always be matched against the entire - recipient address.""")), - - ('max_num_recipients', config.Number, 5, 0, - _('Ceiling on acceptable number of recipients for a posting.'), - - _('''If a posting has this number, or more, of recipients, it is - held for admin approval. Use 0 for no ceiling.''')), - ] - - spam_rtn = [ - _("""This section allows you to configure various anti-spam - filters posting filters, which can help reduce the amount of spam - your list members end up receiving. - """), - - _('Header filters'), - - ('header_filter_rules', config.HeaderFilter, 0, 0, - _('Filter rules to match against the headers of a message.'), - - _("""Each header filter rule has two parts, a list of regular - expressions, one per line, and an action to take. Mailman - matches the message's headers against every regular expression in - the rule and if any match, the message is rejected, held, or - discarded based on the action you specify. Use Defer to - temporarily disable a rule. - - You can have more than one filter rule for your list. In that - case, each rule is matched in turn, with processing stopped after - the first match. - - Note that headers are collected from all the attachments - (except for the mailman administrivia message) and - matched against the regular expressions. With this feature, - you can effectively sort out messages with dangerous file - types or file name extensions.""")), - - _('Legacy anti-spam filters'), - - ('bounce_matching_headers', config.Text, (6, WIDTH), 0, - _('Hold posts with header value matching a specified regexp.'), - _("""Use this option to prohibit posts according to specific - header values. The target value is a regular-expression for - matching against the specified header. The match is done - disregarding letter case. Lines beginning with '#' are ignored - as comments. - -

      For example:

      to: .*@public.com 
      says to hold all - postings with a To: mail header containing '@public.com' - anywhere among the addresses. - -

      Note that leading whitespace is trimmed from the regexp. This - can be circumvented in a number of ways, e.g. by escaping or - bracketing it.""")), - ] - - if subcat == 'sender': - return sender_rtn - elif subcat == 'recipient': - return recip_rtn - elif subcat == 'spam': - return spam_rtn - else: - return subscribing_rtn - - def _setValue(self, mlist, property, val, doc): - # Ignore any hdrfilter_* form variables - if property.startswith('hdrfilter_'): - return - # For subscribe_policy when ALLOW_OPEN_SUBSCRIBE is true, we need to - # add one to the value because the page didn't present an open list as - # an option. - if property == 'subscribe_policy' and not config.ALLOW_OPEN_SUBSCRIBE: - val += 1 - setattr(mlist, property, val) - - # We need to handle the header_filter_rules widgets specially, but - # everything else can be done by the base class's handleForm() method. - # However, to do this we need an awful hack. _setValue() and - # _getValidValue() will essentially ignore any hdrfilter_* form variables. - # TK: we should call this function only in subcat == 'spam' - def _handleForm(self, mlist, category, subcat, cgidata, doc): - # TK: If there is no hdrfilter_* in cgidata, we should not touch - # the header filter rules. - if not cgidata.has_key('hdrfilter_rebox_01'): - return - # First deal with - rules = [] - # We start i at 1 and keep going until we no longer find items keyed - # with the marked tags. - i = 1 - downi = None - while True: - deltag = 'hdrfilter_delete_%02d' % i - reboxtag = 'hdrfilter_rebox_%02d' % i - actiontag = 'hdrfilter_action_%02d' % i - wheretag = 'hdrfilter_where_%02d' % i - addtag = 'hdrfilter_add_%02d' % i - newtag = 'hdrfilter_new_%02d' % i - uptag = 'hdrfilter_up_%02d' % i - downtag = 'hdrfilter_down_%02d' % i - i += 1 - # Was this a delete? If so, we can just ignore this entry - if cgidata.has_key(deltag): - continue - # Get the data for the current box - pattern = cgidata.getvalue(reboxtag) - try: - action = int(cgidata.getvalue(actiontag)) - # We'll get a TypeError when the actiontag is missing and the - # .getvalue() call returns None. - except (ValueError, TypeError): - action = config.DEFER - if pattern is None: - # We came to the end of the boxes - break - if cgidata.has_key(newtag) and not pattern: - # This new entry is incomplete. - if i == 2: - # OK it is the first. - continue - doc.addError(_("""Header filter rules require a pattern. - Incomplete filter rules will be ignored.""")) - continue - # Make sure the pattern was a legal regular expression - try: - re.compile(pattern) - except (re.error, TypeError): - safepattern = Utils.websafe(pattern) - doc.addError(_("""The header filter rule pattern - '%(safepattern)s' is not a legal regular expression. This - rule will be ignored.""")) - continue - # Was this an add item? - if cgidata.has_key(addtag): - # Where should the new one be added? - where = cgidata.getvalue(wheretag) - if where == 'before': - # Add a new empty rule box before the current one - rules.append(('', config.DEFER, True)) - rules.append((pattern, action, False)) - # Default is to add it after... - else: - rules.append((pattern, action, False)) - rules.append(('', config.DEFER, True)) - # Was this an up movement? - elif cgidata.has_key(uptag): - # As long as this one isn't the first rule, move it up - if rules: - rules.insert(-1, (pattern, action, False)) - else: - rules.append((pattern, action, False)) - # Was this the down movement? - elif cgidata.has_key(downtag): - downi = i - 2 - rules.append((pattern, action, False)) - # Otherwise, just retain this one in the list - else: - rules.append((pattern, action, False)) - # Move any down button filter rule - if downi is not None: - rule = rules[downi] - del rules[downi] - rules.insert(downi+1, rule) - mlist.header_filter_rules = rules - - def handleForm(self, mlist, category, subcat, cgidata, doc): - if subcat == 'spam': - self._handleForm(mlist, category, subcat, cgidata, doc) - # Everything else is dealt with by the base handler - GUIBase.handleForm(self, mlist, category, subcat, cgidata, doc) diff --git a/mailman/web/Gui/Topics.py b/mailman/web/Gui/Topics.py deleted file mode 100644 index 00df988be..000000000 --- a/mailman/web/Gui/Topics.py +++ /dev/null @@ -1,162 +0,0 @@ -# Copyright (C) 2001-2009 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 . - -import re - -from Mailman import Utils -from Mailman.Gui.GUIBase import GUIBase -from Mailman.configuration import config -from Mailman.i18n import _ - -OR = '|' - - - -class Topics(GUIBase): - def GetConfigCategory(self): - return 'topics', _('Topics') - - def GetConfigInfo(self, mlist, category, subcat=None): - if category <> 'topics': - return None - WIDTH = config.TEXTFIELDWIDTH - - return [ - _('List topic keywords'), - - ('topics_enabled', config.Radio, (_('Disabled'), _('Enabled')), 0, - _('''Should the topic filter be enabled or disabled?'''), - - _("""The topic filter categorizes each incoming email message - according to regular - expression filters you specify below. If the message's - Subject: or Keywords: header contains a - match against a topic filter, the message is logically placed - into a topic bucket. Each user can then choose to only - receive messages from the mailing list for a particular topic - bucket (or buckets). Any message not categorized in a topic - bucket registered with the user is not delivered to the list. - -

      Note that this feature only works with regular delivery, not - digest delivery. - -

      The body of the message can also be optionally scanned for - Subject: and Keywords: headers, as - specified by the topics_bodylines_limit - configuration variable.""")), - - ('topics_bodylines_limit', config.Number, 5, 0, - _('How many body lines should the topic matcher scan?'), - - _("""The topic matcher will scan this many lines of the message - body looking for topic keyword matches. Body scanning stops when - either this many lines have been looked at, or a non-header-like - body line is encountered. By setting this value to zero, no body - lines will be scanned (i.e. only the Keywords: and - Subject: headers will be scanned). By setting this - value to a negative number, then all body lines will be scanned - until a non-header-like line is encountered. - """)), - - ('topics', config.Topics, 0, 0, - _('Topic keywords, one per line, to match against each message.'), - - _("""Each topic keyword is actually a regular expression, which is - matched against certain parts of a mail message, specifically the - Keywords: and Subject: message headers. - Note that the first few lines of the body of the message can also - contain a Keywords: and Subject: - "header" on which matching is also performed.""")), - - ] - - def handleForm(self, mlist, category, subcat, cgidata, doc): - # MAS: Did we come from the authentication page? - if not cgidata.has_key('topic_box_01'): - return - topics = [] - # We start i at 1 and keep going until we no longer find items keyed - # with the marked tags. - i = 1 - while True: - deltag = 'topic_delete_%02d' % i - boxtag = 'topic_box_%02d' % i - reboxtag = 'topic_rebox_%02d' % i - desctag = 'topic_desc_%02d' % i - wheretag = 'topic_where_%02d' % i - addtag = 'topic_add_%02d' % i - newtag = 'topic_new_%02d' % i - i += 1 - # Was this a delete? If so, we can just ignore this entry - if cgidata.has_key(deltag): - continue - # Get the data for the current box - name = cgidata.getvalue(boxtag) - pattern = cgidata.getvalue(reboxtag) - desc = cgidata.getvalue(desctag) - if name is None: - # We came to the end of the boxes - break - if cgidata.has_key(newtag) and (not name or not pattern): - # This new entry is incomplete. - doc.addError(_("""Topic specifications require both a name and - a pattern. Incomplete topics will be ignored.""")) - continue - # Make sure the pattern was a legal regular expression - name = Utils.websafe(name) - try: - orpattern = OR.join(pattern.splitlines()) - re.compile(orpattern) - except (re.error, TypeError): - safepattern = Utils.websafe(orpattern) - doc.addError(_("""The topic pattern '%(safepattern)s' is not a - legal regular expression. It will be discarded.""")) - continue - # Was this an add item? - if cgidata.has_key(addtag): - # Where should the new one be added? - where = cgidata.getvalue(wheretag) - if where == 'before': - # Add a new empty topics box before the current one - topics.append(('', '', '', True)) - topics.append((name, pattern, desc, False)) - # Default is to add it after... - else: - topics.append((name, pattern, desc, False)) - topics.append(('', '', '', True)) - # Otherwise, just retain this one in the list - else: - topics.append((name, pattern, desc, False)) - # Add these topics to the mailing list object, and deal with other - # options. - mlist.topics = topics - try: - mlist.topics_enabled = int(cgidata.getvalue( - 'topics_enabled', - mlist.topics_enabled)) - except ValueError: - # BAW: should really print a warning - pass - try: - mlist.topics_bodylines_limit = int(cgidata.getvalue( - 'topics_bodylines_limit', - mlist.topics_bodylines_limit)) - except ValueError: - # BAW: should really print a warning - pass diff --git a/mailman/web/Gui/Usenet.py b/mailman/web/Gui/Usenet.py deleted file mode 100644 index 9c1b50809..000000000 --- a/mailman/web/Gui/Usenet.py +++ /dev/null @@ -1,140 +0,0 @@ -# Copyright (C) 2001-2009 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 . - -from Mailman.Gui.GUIBase import GUIBase -from Mailman.configuration import config -from Mailman.i18n import _ - - - -class Usenet(GUIBase): - def GetConfigCategory(self): - return 'gateway', _('Mail<->News gateways') - - def GetConfigInfo(self, mlist, category, subcat=None): - if category <> 'gateway': - return None - - WIDTH = config.TEXTFIELDWIDTH - VERTICAL = 1 - - return [ - _('Mail-to-News and News-to-Mail gateway services.'), - - _('News server settings'), - - ('nntp_host', config.String, WIDTH, 0, - _('The hostname of the machine your news server is running on.'), - _('''This value may be either the name of your news server, or - optionally of the format name:port, where port is a port number. - - The news server is not part of Mailman proper. You have to - already have access to an NNTP server, and that NNTP server must - recognize the machine this mailing list runs on as a machine - capable of reading and posting news.''')), - - ('linked_newsgroup', config.String, WIDTH, 0, - _('The name of the Usenet group to gateway to and/or from.')), - - ('gateway_to_news', config.Toggle, (_('No'), _('Yes')), 0, - _('''Should new posts to the mailing list be sent to the - newsgroup?''')), - - ('gateway_to_mail', config.Toggle, (_('No'), _('Yes')), 0, - _('''Should new posts to the newsgroup be sent to the mailing - list?''')), - - _('Forwarding options'), - - ('news_moderation', config.Radio, - (_('None'), _('Open list, moderated group'), _('Moderated')), - VERTICAL, - - _("""The moderation policy of the newsgroup."""), - - _("""This setting determines the moderation policy of the - newsgroup and its interaction with the moderation policy of the - mailing list. This only applies to the newsgroup that you are - gatewaying to, so if you are only gatewaying from - Usenet, or the newsgroup you are gatewaying to is not moderated, - set this option to None. - -

      If the newsgroup is moderated, you can set this mailing list - up to be the moderation address for the newsgroup. By selecting - Moderated, an additional posting hold will be placed in - the approval process. All messages posted to the mailing list - will have to be approved before being sent on to the newsgroup, - or to the mailing list membership. - -

      Note that if the message has an Approved header - with the list's administrative password in it, this hold test - will be bypassed, allowing privileged posters to send messages - directly to the list and the newsgroup. - -

      Finally, if the newsgroup is moderated, but you want to have - an open posting policy anyway, you should select Open list, - moderated group. The effect of this is to use the normal - Mailman moderation facilities, but to add an Approved - header to all messages that are gatewayed to Usenet.""")), - - ('news_prefix_subject_too', config.Toggle, (_('No'), _('Yes')), 0, - _('Prefix Subject: headers on postings gated to news?'), - _("""Mailman prefixes Subject: headers with - text you can - customize and normally, this prefix shows up in messages - gatewayed to Usenet. You can set this option to No to - disable the prefix on gated messages. Of course, if you turn off - normal Subject: prefixes, they won't be prefixed for - gated messages either.""")), - - _('Mass catch up'), - - ('_mass_catchup', config.Toggle, (_('No'), _('Yes')), 0, - _('Should Mailman perform a catchup on the newsgroup?'), - _('''When you tell Mailman to perform a catchup on the newsgroup, - this means that you want to start gating messages to the mailing - list with the next new message found. All earlier messages on - the newsgroup will be ignored. This is as if you were reading - the newsgroup yourself, and you marked all current messages as - read. By catching up, your mailing list members will - not see any of the earlier messages.''')), - - ] - - def _setValue(self, mlist, property, val, doc): - # Watch for the special, immediate action attributes - if property == '_mass_catchup' and val: - mlist.usenet_watermark = None - doc.AddItem(_('Mass catchup completed')) - else: - GUIBase._setValue(self, mlist, property, val, doc) - - def _postValidate(self, mlist, doc): - # Make sure that if we're gating, that the newsgroups and host - # information are not blank. - if mlist.gateway_to_news or mlist.gateway_to_mail: - # BAW: It's too expensive and annoying to ensure that both the - # host is valid and that the newsgroup is a valid n.g. on the - # server. This should be good enough. - if not mlist.nntp_host or not mlist.linked_newsgroup: - doc.addError(_("""You cannot enable gatewaying unless both the - news server field and - the linked - newsgroup fields are filled in.""")) - # And reset these values - mlist.gateway_to_news = 0 - mlist.gateway_to_mail = 0 diff --git a/mailman/web/Gui/__init__.py b/mailman/web/Gui/__init__.py deleted file mode 100644 index 2e12526c0..000000000 --- a/mailman/web/Gui/__init__.py +++ /dev/null @@ -1,33 +0,0 @@ -# Copyright (C) 2001-2009 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 . - -from Archive import Archive -from Autoresponse import Autoresponse -from Bounce import Bounce -from Digest import Digest -from General import General -from Membership import Membership -from NonDigest import NonDigest -from Passwords import Passwords -from Privacy import Privacy -from Topics import Topics -from Usenet import Usenet -from Language import Language -from ContentFilter import ContentFilter - -# Don't export this symbol outside the package -del GUIBase diff --git a/mailman/web/HTMLFormatter.py b/mailman/web/HTMLFormatter.py deleted file mode 100644 index 594f4adea..000000000 --- a/mailman/web/HTMLFormatter.py +++ /dev/null @@ -1,437 +0,0 @@ -# Copyright (C) 1998-2009 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 . - -"""Routines for presentation of list-specific HTML text.""" - -import re -import time - -from mailman import Defaults -from mailman import MemberAdaptor -from mailman import Utils -from mailman.configuration import config -from mailman.htmlformat import * -from mailman.i18n import _ - - -EMPTYSTRING = '' -BR = '
      ' -NL = '\n' -COMMASPACE = ', ' - - - -class HTMLFormatter: - def GetMailmanFooter(self): - ownertext = COMMASPACE.join([Utils.ObscureEmail(a, 1) - for a in self.owner]) - # Remove the .Format() when htmlformat conversion is done. - realname = self.real_name - hostname = self.host_name - listinfo_link = Link(self.GetScriptURL('listinfo'), - realname).Format() - owner_link = Link('mailto:' + self.GetOwnerEmail(), ownertext).Format() - innertext = _('%(listinfo_link)s list run by %(owner_link)s') - return Container( - '


      ', - Address( - Container( - innertext, - '
      ', - Link(self.GetScriptURL('admin'), - _('%(realname)s administrative interface')), - _(' (requires authorization)'), - '
      ', - Link(Utils.ScriptURL('listinfo'), - _('Overview of all %(hostname)s mailing lists')), - '

      ', MailmanLogo()))).Format() - - def FormatUsers(self, digest, lang=None): - if lang is None: - lang = self.preferred_language - conceal_sub = Defaults.ConcealSubscription - people = [] - if digest: - digestmembers = self.getDigestMemberKeys() - for dm in digestmembers: - if not self.getMemberOption(dm, conceal_sub): - people.append(dm) - num_concealed = len(digestmembers) - len(people) - else: - members = self.getRegularMemberKeys() - for m in members: - if not self.getMemberOption(m, conceal_sub): - people.append(m) - num_concealed = len(members) - len(people) - if num_concealed == 1: - concealed = _('(1 private member not shown)') - elif num_concealed > 1: - concealed = _( - '(%(num_concealed)d private members not shown)') - else: - concealed = '' - items = [] - people.sort() - obscure = self.obscure_addresses - for person in people: - id = Utils.ObscureEmail(person) - url = self.GetOptionsURL(person, obscure=obscure) - if obscure: - showing = Utils.ObscureEmail(person, for_text=1) - else: - showing = person - got = Link(url, showing) - if self.getDeliveryStatus(person) <> MemberAdaptor.ENABLED: - got = Italic('(', got, ')') - items.append(got) - # Just return the .Format() so this works until I finish - # converting everything to htmlformat... - return concealed + UnorderedList(*tuple(items)).Format() - - def FormatOptionButton(self, option, value, user): - if option == Defaults.DisableDelivery: - optval = self.getDeliveryStatus(user) <> MemberAdaptor.ENABLED - else: - optval = self.getMemberOption(user, option) - if optval == value: - checked = ' CHECKED' - else: - checked = '' - name = { - Defaults.DontReceiveOwnPosts : 'dontreceive', - Defaults.DisableDelivery : 'disablemail', - Defaults.DisableMime : 'mime', - Defaults.AcknowledgePosts : 'ackposts', - Defaults.Digests : 'digest', - Defaults.ConcealSubscription : 'conceal', - Defaults.SuppressPasswordReminder : 'remind', - Defaults.ReceiveNonmatchingTopics : 'rcvtopic', - Defaults.DontReceiveDuplicates : 'nodupes', - }[option] - return '' % ( - name, value, checked) - - def FormatDigestButton(self): - if self.digest_is_default: - checked = ' CHECKED' - else: - checked = '' - return '' % checked - - def FormatDisabledNotice(self, user): - status = self.getDeliveryStatus(user) - reason = None - info = self.getBounceInfo(user) - if status == MemberAdaptor.BYUSER: - reason = _('; it was disabled by you') - elif status == MemberAdaptor.BYADMIN: - reason = _('; it was disabled by the list administrator') - elif status == MemberAdaptor.BYBOUNCE: - date = time.strftime('%d-%b-%Y', - time.localtime(Utils.midnight(info.date))) - reason = _('''; it was disabled due to excessive bounces. The - last bounce was received on %(date)s''') - elif status == MemberAdaptor.UNKNOWN: - reason = _('; it was disabled for unknown reasons') - if reason: - note = FontSize('+1', _( - 'Note: your list delivery is currently disabled%(reason)s.' - )).Format() - link = Link('#disable', _('Mail delivery')).Format() - mailto = Link('mailto:' + self.GetOwnerEmail(), - _('the list administrator')).Format() - return _('''

      %(note)s - -

      You may have disabled list delivery intentionally, - or it may have been triggered by bounces from your email - address. In either case, to re-enable delivery, change the - %(link)s option below. Contact %(mailto)s if you have any - questions or need assistance.''') - elif info and info.score > 0: - # Provide information about their current bounce score. We know - # their membership is currently enabled. - score = info.score - total = self.bounce_score_threshold - return _('''

      We have received some recent bounces from your - address. Your current bounce score is %(score)s out of a - maximum of %(total)s. Please double check that your subscribed - address is correct and that there are no problems with delivery to - this address. Your bounce score will be automatically reset if - the problems are corrected soon.''') - else: - return '' - - def FormatUmbrellaNotice(self, user, type): - addr = self.GetMemberAdminEmail(user) - if self.umbrella_list: - return _("(Note - you are subscribing to a list of mailing lists, " - "so the %(type)s notice will be sent to the admin address" - " for your membership, %(addr)s.)

      ") - else: - return "" - - def FormatSubscriptionMsg(self): - msg = '' - also = '' - if self.subscribe_policy == 1: - msg += _('''You will be sent email requesting confirmation, to - prevent others from gratuitously subscribing you.''') - elif self.subscribe_policy == 2: - msg += _("""This is a closed list, which means your subscription - will be held for approval. You will be notified of the list - moderator's decision by email.""") - also = _('also ') - elif self.subscribe_policy == 3: - msg += _("""You will be sent email requesting confirmation, to - prevent others from gratuitously subscribing you. Once - confirmation is received, your request will be held for approval - by the list moderator. You will be notified of the moderator's - decision by email.""") - also = _("also ") - if msg: - msg += ' ' - if self.private_roster == 1: - msg += _('''This is %(also)sa private list, which means that the - list of members is not available to non-members.''') - elif self.private_roster: - msg += _('''This is %(also)sa hidden list, which means that the - list of members is available only to the list administrator.''') - else: - msg += _('''This is %(also)sa public list, which means that the - list of members list is available to everyone.''') - if self.obscure_addresses: - msg += _(''' (but we obscure the addresses so they are not - easily recognizable by spammers).''') - - if self.umbrella_list: - sfx = self.umbrella_member_suffix - msg += _("""

      (Note that this is an umbrella list, intended to - have only other mailing lists as members. Among other things, - this means that your confirmation request will be sent to the - `%(sfx)s' account for your address.)""") - return msg - - def FormatUndigestButton(self): - if self.digest_is_default: - checked = '' - else: - checked = ' CHECKED' - return '' % checked - - def FormatMimeDigestsButton(self): - if self.mime_is_default_digest: - checked = ' CHECKED' - else: - checked = '' - return '' % checked - - def FormatPlainDigestsButton(self): - if self.mime_is_default_digest: - checked = '' - else: - checked = ' CHECKED' - return '' % checked - - def FormatEditingOption(self, lang): - if self.private_roster == 0: - either = _('either ') - else: - either = '' - realname = self.real_name - - text = (_('''To unsubscribe from %(realname)s, get a password reminder, - or change your subscription options %(either)senter your subscription - email address: -

      ''') - + TextBox('email', size=30).Format() - + ' ' - + SubmitButton('UserOptions', - _('Unsubscribe or edit options')).Format() - + Hidden('language', lang).Format() - + '
      ') - if self.private_roster == 0: - text += _('''

      ... or select your entry from - the subscribers list (see above).''') - text += _(''' If you leave the field blank, you will be prompted for - your email address''') - return text - - def RestrictedListMessage(self, which, restriction): - if not restriction: - return '' - elif restriction == 1: - return _( - '''(%(which)s is only available to the list - members.)''') - else: - return _('''(%(which)s is only available to the list - administrator.)''') - - def FormatRosterOptionForUser(self, lang): - return self.RosterOption(lang).Format() - - def RosterOption(self, lang): - container = Container() - container.AddItem(Hidden('language', lang)) - if not self.private_roster: - container.AddItem(_("Click here for the list of ") - + self.real_name - + _(" subscribers: ")) - container.AddItem(SubmitButton('SubscriberRoster', - _("Visit Subscriber list"))) - else: - if self.private_roster == 1: - only = _('members') - whom = _('Address:') - else: - only = _('the list administrator') - whom = _('Admin address:') - # Solicit the user and password. - container.AddItem( - self.RestrictedListMessage(_('The subscribers list'), - self.private_roster) - + _("

      Enter your ") - + whom[:-1].lower() - + _(" and password to visit" - " the subscribers list:

      ") - + whom - + " ") - container.AddItem(self.FormatBox('roster-email')) - container.AddItem(_("Password: ") - + self.FormatSecureBox('roster-pw') - + "  ") - container.AddItem(SubmitButton('SubscriberRoster', - _('Visit Subscriber List'))) - container.AddItem("
      ") - return container - - def FormatFormStart(self, name, extra=''): - base_url = self.GetScriptURL(name) - if extra: - full_url = "%s/%s" % (base_url, extra) - else: - full_url = base_url - return ('
      ' % full_url) - - def FormatArchiveAnchor(self): - return '' % self.GetBaseArchiveURL() - - def FormatFormEnd(self): - return '' - - def FormatBox(self, name, size=20, value=''): - return '' % ( - name, size, value) - - def FormatSecureBox(self, name): - return '' % name - - def FormatButton(self, name, text='Submit'): - return '' % (name, text) - - def FormatReminder(self, lang): - if self.send_reminders: - return _('Once a month, your password will be emailed to you as' - ' a reminder.') - return '' - - def ParseTags(self, template, replacements, lang=None): - if lang is None: - charset = 'us-ascii' - else: - charset = Utils.GetCharSet(lang) - text = Utils.maketext(template, raw=1, lang=lang, mlist=self) - parts = re.split('(]*>)', text) - i = 1 - while i < len(parts): - tag = parts[i].lower() - if replacements.has_key(tag): - repl = replacements[tag] - if isinstance(repl, str): - repl = unicode(repl, charset, 'replace') - parts[i] = repl - else: - parts[i] = '' - i = i + 2 - return EMPTYSTRING.join(parts) - - # This needs to wait until after the list is inited, so let's build it - # when it's needed only. - def GetStandardReplacements(self, lang=None): - dmember_len = len(self.getDigestMemberKeys()) - member_len = len(self.getRegularMemberKeys()) - # If only one language is enabled for this mailing list, omit the - # language choice buttons. - if len(self.language_codes) == 1: - listlangs = _( - config.languages.get_description(self.preferred_language)) - else: - listlangs = self.GetLangSelectBox(lang).Format() - d = { - '' : self.GetMailmanFooter(), - '' : self.real_name, - '' : self._internal_name, - '' : self.description, - '' : BR.join(self.info.split(NL)), - '' : self.FormatFormEnd(), - '' : self.FormatArchiveAnchor(), - '' : '
      ', - '' : self.FormatSubscriptionMsg(), - '' : \ - self.RestrictedListMessage(_('The current archive'), - self.archive_private), - '' : `member_len`, - '' : `dmember_len`, - '' : (`member_len + dmember_len`), - '' : '%s' % self.GetListEmail(), - '' : '%s' % self.GetRequestEmail(), - '' : self.GetOwnerEmail(), - '' : self.FormatReminder(self.preferred_language), - '' : self.host_name, - '' : listlangs, - } - if config.IMAGE_LOGOS: - d[''] = config.IMAGE_LOGOS + config.SHORTCUT_ICON - return d - - def GetAllReplacements(self, lang=None): - """ - returns standard replaces plus formatted user lists in - a dict just like GetStandardReplacements. - """ - if lang is None: - lang = self.preferred_language - d = self.GetStandardReplacements(lang) - d.update({"": self.FormatUsers(0, lang), - "": self.FormatUsers(1, lang)}) - return d - - def GetLangSelectBox(self, lang=None, varname='language'): - if lang is None: - lang = self.preferred_language - # Figure out the available languages - values = self.language_codes - legend = [config.languages.get_description(code) for code in values] - try: - selected = values.index(lang) - except ValueError: - try: - selected = values.index(self.preferred_language) - except ValueError: - selected = config.DEFAULT_SERVER_LANGUAGE - # Return the widget - return SelectOptions(varname, values, legend, selected) diff --git a/mailman/web/__init__.py b/mailman/web/__init__.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/mailman/web/htmlformat.py b/mailman/web/htmlformat.py deleted file mode 100644 index 608d0e647..000000000 --- a/mailman/web/htmlformat.py +++ /dev/null @@ -1,670 +0,0 @@ -# Copyright (C) 1998-2009 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 . - -"""Library for program-based construction of an HTML documents. - -Encapsulate HTML formatting directives in classes that act as containers -for python and, recursively, for nested HTML formatting objects. -""" - -from Mailman import Defaults -from Mailman import Utils -from Mailman import version -from Mailman.configuration import config -from Mailman.i18n import _ - -SPACE = ' ' -EMPTYSTRING = '' -NL = '\n' - - - -# Format an arbitrary object. -def HTMLFormatObject(item, indent): - "Return a presentation of an object, invoking their Format method if any." - if hasattr(item, 'Format'): - return item.Format(indent) - if isinstance(item, basestring): - return item - return str(item) - -def CaseInsensitiveKeyedDict(d): - result = {} - for (k,v) in d.items(): - result[k.lower()] = v - return result - -# Given references to two dictionaries, copy the second dictionary into the -# first one. -def DictMerge(destination, fresh_dict): - for (key, value) in fresh_dict.items(): - destination[key] = value - -class Table: - def __init__(self, **table_opts): - self.cells = [] - self.cell_info = {} - self.row_info = {} - self.opts = table_opts - - def AddOptions(self, opts): - DictMerge(self.opts, opts) - - # Sets all of the cells. It writes over whatever cells you had there - # previously. - - def SetAllCells(self, cells): - self.cells = cells - - # Add a new blank row at the end - def NewRow(self): - self.cells.append([]) - - # Add a new blank cell at the end - def NewCell(self): - self.cells[-1].append('') - - def AddRow(self, row): - self.cells.append(row) - - def AddCell(self, cell): - self.cells[-1].append(cell) - - def AddCellInfo(self, row, col, **kws): - kws = CaseInsensitiveKeyedDict(kws) - if not self.cell_info.has_key(row): - self.cell_info[row] = { col : kws } - elif self.cell_info[row].has_key(col): - DictMerge(self.cell_info[row], kws) - else: - self.cell_info[row][col] = kws - - def AddRowInfo(self, row, **kws): - kws = CaseInsensitiveKeyedDict(kws) - if not self.row_info.has_key(row): - self.row_info[row] = kws - else: - DictMerge(self.row_info[row], kws) - - # What's the index for the row we just put in? - def GetCurrentRowIndex(self): - return len(self.cells)-1 - - # What's the index for the col we just put in? - def GetCurrentCellIndex(self): - return len(self.cells[-1])-1 - - def ExtractCellInfo(self, info): - valid_mods = ['align', 'valign', 'nowrap', 'rowspan', 'colspan', - 'bgcolor'] - output = '' - - for (key, val) in info.items(): - if not key in valid_mods: - continue - if key == 'nowrap': - output = output + ' NOWRAP' - continue - else: - output = output + ' %s="%s"' % (key.upper(), val) - - return output - - def ExtractRowInfo(self, info): - valid_mods = ['align', 'valign', 'bgcolor'] - output = '' - - for (key, val) in info.items(): - if not key in valid_mods: - continue - output = output + ' %s="%s"' % (key.upper(), val) - - return output - - def ExtractTableInfo(self, info): - valid_mods = ['align', 'width', 'border', 'cellspacing', 'cellpadding', - 'bgcolor'] - - output = '' - - for (key, val) in info.items(): - if not key in valid_mods: - continue - if key == 'border' and val == None: - output = output + ' BORDER' - continue - else: - output = output + ' %s="%s"' % (key.upper(), val) - - return output - - def FormatCell(self, row, col, indent): - try: - my_info = self.cell_info[row][col] - except: - my_info = None - - output = '\n' + ' '*indent + '' - - for i in range(len(self.cells[row])): - output = output + self.FormatCell(row, i, indent + 2) - - output = output + '\n' + ' '*indent + '' - - return output - - def Format(self, indent=0): - output = '\n' + ' '*indent + '' - - for i in range(len(self.cells)): - output = output + self.FormatRow(i, indent + 2) - - output = output + '\n' + ' '*indent + '\n' - - return output - - -class Link: - def __init__(self, href, text, target=None): - self.href = href - self.text = text - self.target = target - - def Format(self, indent=0): - texpr = "" - if self.target != None: - texpr = ' target="%s"' % self.target - return '%s' % (HTMLFormatObject(self.href, indent), - texpr, - HTMLFormatObject(self.text, indent)) - -class FontSize: - """FontSize is being deprecated - use FontAttr(..., size="...") instead.""" - def __init__(self, size, *items): - self.items = list(items) - self.size = size - - def Format(self, indent=0): - output = '' % self.size - for item in self.items: - output = output + HTMLFormatObject(item, indent) - output = output + '' - return output - -class FontAttr: - """Present arbitrary font attributes.""" - def __init__(self, *items, **kw): - self.items = list(items) - self.attrs = kw - - def Format(self, indent=0): - seq = [] - for k, v in self.attrs.items(): - seq.append('%s="%s"' % (k, v)) - output = '' % SPACE.join(seq) - for item in self.items: - output = output + HTMLFormatObject(item, indent) - output = output + '' - return output - - -class Container: - def __init__(self, *items): - if not items: - self.items = [] - else: - self.items = items - - def AddItem(self, obj): - self.items.append(obj) - - def Format(self, indent=0): - output = [] - for item in self.items: - output.append(HTMLFormatObject(item, indent)) - return EMPTYSTRING.join(output) - - -class Label(Container): - align = 'right' - - def __init__(self, *items): - Container.__init__(self, *items) - - def Format(self, indent=0): - return ('
      ' % self.align) + \ - Container.Format(self, indent) + \ - '
      ' - - -# My own standard document template. YMMV. -# something more abstract would be more work to use... - -class Document(Container): - title = None - language = None - bgcolor = Defaults.WEB_BG_COLOR - suppress_head = 0 - - def set_language(self, lang=None): - self.language = lang - - def set_bgcolor(self, color): - self.bgcolor = color - - def SetTitle(self, title): - self.title = title - - def Format(self, indent=0, **kws): - charset = 'us-ascii' - if self.language: - charset = Utils.GetCharSet(self.language) - output = ['Content-Type: text/html; charset=%s\n' % charset] - if not self.suppress_head: - kws.setdefault('bgcolor', self.bgcolor) - tab = ' ' * indent - output.extend([tab, - '', - '' - ]) - if config.IMAGE_LOGOS: - output.append('' % - (config.IMAGE_LOGOS + config.SHORTCUT_ICON)) - # Hit all the bases - output.append('' % charset) - if self.title: - output.append('%s%s' % (tab, self.title)) - output.append('%s' % tab) - quals = [] - # Default link colors - if config.WEB_VLINK_COLOR: - kws.setdefault('vlink', config.WEB_VLINK_COLOR) - if config.WEB_ALINK_COLOR: - kws.setdefault('alink', config.WEB_ALINK_COLOR) - if config.WEB_LINK_COLOR: - kws.setdefault('link', config.WEB_LINK_COLOR) - for k, v in kws.items(): - quals.append('%s="%s"' % (k, v)) - output.append('%s' % (tab, SPACE.join(quals))) - # Always do this... - output.append(Container.Format(self, indent)) - if not self.suppress_head: - output.append('%s' % tab) - output.append('%s' % tab) - return NL.join(output).encode(charset, 'replace') - - def addError(self, errmsg, tag=None): - if tag is None: - tag = _('Error: ') - self.AddItem(Header(3, Bold(FontAttr( - _(tag), color=config.WEB_ERROR_COLOR, size='+2')).Format() + - Italic(errmsg).Format())) - - -class HeadlessDocument(Document): - """Document without head section, for templates that provide their own.""" - suppress_head = 1 - - -class StdContainer(Container): - def Format(self, indent=0): - # If I don't start a new I ignore indent - output = '<%s>' % self.tag - output = output + Container.Format(self, indent) - output = '%s' % (output, self.tag) - return output - - -class QuotedContainer(Container): - def Format(self, indent=0): - # If I don't start a new I ignore indent - output = '<%s>%s' % ( - self.tag, - Utils.websafe(Container.Format(self, indent)), - self.tag) - return output - -class Header(StdContainer): - def __init__(self, num, *items): - self.items = items - self.tag = 'h%d' % num - -class Address(StdContainer): - tag = 'address' - -class Underline(StdContainer): - tag = 'u' - -class Bold(StdContainer): - tag = 'strong' - -class Italic(StdContainer): - tag = 'em' - -class Preformatted(QuotedContainer): - tag = 'pre' - -class Subscript(StdContainer): - tag = 'sub' - -class Superscript(StdContainer): - tag = 'sup' - -class Strikeout(StdContainer): - tag = 'strike' - -class Center(StdContainer): - tag = 'center' - -class Form(Container): - def __init__(self, action='', method='POST', encoding=None, *items): - apply(Container.__init__, (self,) + items) - self.action = action - self.method = method - self.encoding = encoding - - def set_action(self, action): - self.action = action - - def Format(self, indent=0): - spaces = ' ' * indent - encoding = '' - if self.encoding: - encoding = 'enctype="%s"' % self.encoding - output = '\n%s
      \n' % ( - spaces, self.action, self.method, encoding) - output = output + Container.Format(self, indent+2) - output = '%s\n%s
      \n' % (output, spaces) - return output - - -class InputObj: - def __init__(self, name, ty, value, checked, **kws): - self.name = name - self.type = ty - self.value = value - self.checked = checked - self.kws = kws - - def Format(self, indent=0): - output = ['') - return SPACE.join(output) - - -class SubmitButton(InputObj): - def __init__(self, name, button_text): - InputObj.__init__(self, name, "SUBMIT", button_text, checked=0) - -class PasswordBox(InputObj): - def __init__(self, name, value='', size=Defaults.TEXTFIELDWIDTH): - InputObj.__init__(self, name, "PASSWORD", value, checked=0, size=size) - -class TextBox(InputObj): - def __init__(self, name, value='', size=Defaults.TEXTFIELDWIDTH): - InputObj.__init__(self, name, "TEXT", value, checked=0, size=size) - -class Hidden(InputObj): - def __init__(self, name, value=''): - InputObj.__init__(self, name, 'HIDDEN', value, checked=0) - -class TextArea: - def __init__(self, name, text='', rows=None, cols=None, wrap='soft', - readonly=0): - self.name = name - self.text = text - self.rows = rows - self.cols = cols - self.wrap = wrap - self.readonly = readonly - - def Format(self, indent=0): - output = '' % self.text + return output + +class FileUpload(InputObj): + def __init__(self, name, rows=None, cols=None, **kws): + apply(InputObj.__init__, (self, name, 'FILE', '', 0), kws) + +class RadioButton(InputObj): + def __init__(self, name, value, checked=0, **kws): + apply(InputObj.__init__, (self, name, 'RADIO', value, checked), kws) + +class CheckBox(InputObj): + def __init__(self, name, value, checked=0, **kws): + apply(InputObj.__init__, (self, name, "CHECKBOX", value, checked), kws) + +class VerticalSpacer: + def __init__(self, size=10): + self.size = size + def Format(self, indent=0): + output = '' % self.size + return output + +class WidgetArray: + Widget = None + + def __init__(self, name, button_names, checked, horizontal, values): + self.name = name + self.button_names = button_names + self.checked = checked + self.horizontal = horizontal + self.values = values + assert len(values) == len(button_names) + # Don't assert `checked' because for RadioButtons it is a scalar while + # for CheckedBoxes it is a vector. Subclasses will assert length. + + def ischecked(self, i): + raise NotImplemented + + def Format(self, indent=0): + t = Table(cellspacing=5) + items = [] + for i, name, value in zip(range(len(self.button_names)), + self.button_names, + self.values): + ischecked = (self.ischecked(i)) + item = self.Widget(self.name, value, ischecked).Format() + name + items.append(item) + if not self.horizontal: + t.AddRow(items) + items = [] + if self.horizontal: + t.AddRow(items) + return t.Format(indent) + +class RadioButtonArray(WidgetArray): + Widget = RadioButton + + def __init__(self, name, button_names, checked=None, horizontal=1, + values=None): + if values is None: + values = range(len(button_names)) + # BAW: assert checked is a scalar... + WidgetArray.__init__(self, name, button_names, checked, horizontal, + values) + + def ischecked(self, i): + return self.checked == i + +class CheckBoxArray(WidgetArray): + Widget = CheckBox + + def __init__(self, name, button_names, checked=None, horizontal=0, + values=None): + if checked is None: + checked = [0] * len(button_names) + else: + assert len(checked) == len(button_names) + if values is None: + values = range(len(button_names)) + WidgetArray.__init__(self, name, button_names, checked, horizontal, + values) + + def ischecked(self, i): + return self.checked[i] + +class UnorderedList(Container): + def Format(self, indent=0): + spaces = ' ' * indent + output = '\n%s
        \n' % spaces + for item in self.items: + output = output + '%s
      • %s\n' % \ + (spaces, HTMLFormatObject(item, indent + 2)) + output = output + '%s
      \n' % spaces + return output + +class OrderedList(Container): + def Format(self, indent=0): + spaces = ' ' * indent + output = '\n%s
        \n' % spaces + for item in self.items: + output = output + '%s
      1. %s\n' % \ + (spaces, HTMLFormatObject(item, indent + 2)) + output = output + '%s
      \n' % spaces + return output + +class DefinitionList(Container): + def Format(self, indent=0): + spaces = ' ' * indent + output = '\n%s
      \n' % spaces + for dt, dd in self.items: + output = output + '%s
      %s\n
      %s\n' % \ + (spaces, HTMLFormatObject(dt, indent+2), + HTMLFormatObject(dd, indent+2)) + output = output + '%s
      \n' % spaces + return output + + + +# Logo constants +# +# These are the URLs which the image logos link to. The Mailman home page now +# points at the gnu.org site instead of the www.list.org mirror. + +PYTHON_URL = 'http://www.python.org/' +GNU_URL = 'http://www.gnu.org/' + +# The names of the image logo files. These are concatentated onto +# config.IMAGE_LOGOS (not urljoined). +DELIVERED_BY = 'mailman.jpg' +PYTHON_POWERED = 'PythonPowered.png' +GNU_HEAD = 'gnu-head-tiny.jpg' + + +def MailmanLogo(): + t = Table(border=0, width='100%') + if config.IMAGE_LOGOS: + def logo(file): + return config.IMAGE_LOGOS + file + mmlink = 'Delivered by Mailman' \ + '
      version %s' % (logo(DELIVERED_BY), version.VERSION) + pylink = 'Python Powered' % \ + logo(PYTHON_POWERED) + gnulink = 'GNU\'s Not Unix' % \ + logo(GNU_HEAD) + t.AddRow([mmlink, pylink, gnulink]) + else: + # use only textual links + version = version.VERSION + mmlink = Link(config.MAILMAN_URL, + _('Delivered by Mailman
      version %(version)s')) + pylink = Link(PYTHON_URL, _('Python Powered')) + gnulink = Link(GNU_URL, _("Gnu's Not Unix")) + t.AddRow([mmlink, pylink, gnulink]) + return t + + +class SelectOptions: + def __init__(self, varname, values, legend, + selected=0, size=1, multiple=None): + self.varname = varname + self.values = values + self.legend = legend + self.size = size + self.multiple = multiple + # we convert any type to tuple, commas are needed + if not multiple: + if isinstance(selected, int): + self.selected = (selected,) + elif isinstance(selected, tuple): + self.selected = (selected[0],) + elif isinstance(selected, list): + self.selected = (selected[0],) + else: + self.selected = (0,) + + def Format(self, indent=0): + spaces = " " * indent + items = min( len(self.values), len(self.legend) ) + + # jcrey: If there is no argument, we return nothing to avoid errors + if items == 0: + return "" + + text = "\n" + spaces + "' -- cgit v1.3.1