summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/mailman/__init__.py7
-rw-r--r--src/mailman/app/bounces.py15
-rw-r--r--src/mailman/app/commands.py6
-rw-r--r--src/mailman/app/docs/hooks.rst5
-rw-r--r--src/mailman/app/docs/pipelines.rst16
-rw-r--r--src/mailman/app/docs/subscriptions.rst7
-rw-r--r--src/mailman/app/domain.py6
-rw-r--r--src/mailman/app/events.py6
-rw-r--r--src/mailman/app/inject.py14
-rw-r--r--src/mailman/app/lifecycle.py6
-rw-r--r--src/mailman/app/membership.py6
-rw-r--r--src/mailman/app/moderator.py12
-rw-r--r--src/mailman/app/notifications.py9
-rw-r--r--src/mailman/app/registrar.py10
-rw-r--r--src/mailman/app/replybot.py3
-rw-r--r--src/mailman/app/subscriptions.py9
-rw-r--r--src/mailman/app/templates.py43
-rw-r--r--src/mailman/app/tests/test_bounces.py14
-rw-r--r--src/mailman/app/tests/test_inject.py15
-rw-r--r--src/mailman/app/tests/test_lifecycle.py3
-rw-r--r--src/mailman/app/tests/test_membership.py6
-rw-r--r--src/mailman/app/tests/test_moderation.py6
-rw-r--r--src/mailman/app/tests/test_notifications.py7
-rw-r--r--src/mailman/app/tests/test_registration.py8
-rw-r--r--src/mailman/app/tests/test_subscriptions.py18
-rw-r--r--src/mailman/app/tests/test_templates.py27
-rw-r--r--src/mailman/archiving/mailarchive.py11
-rw-r--r--src/mailman/archiving/mhonarc.py10
-rw-r--r--src/mailman/archiving/prototype.py12
-rw-r--r--src/mailman/archiving/tests/test_prototype.py12
-rw-r--r--src/mailman/bin/export.py2
-rw-r--r--src/mailman/bin/gate_news.py2
-rw-r--r--src/mailman/bin/mailman.py16
-rw-r--r--src/mailman/bin/master.py14
-rw-r--r--src/mailman/bin/onebounce.py3
-rw-r--r--src/mailman/bin/runner.py3
-rw-r--r--src/mailman/bin/tests/test_master.py3
-rw-r--r--src/mailman/chains/accept.py6
-rw-r--r--src/mailman/chains/base.py6
-rw-r--r--src/mailman/chains/builtin.py6
-rw-r--r--src/mailman/chains/discard.py5
-rw-r--r--src/mailman/chains/headers.py8
-rw-r--r--src/mailman/chains/hold.py12
-rw-r--r--src/mailman/chains/moderation.py6
-rw-r--r--src/mailman/chains/owner.py6
-rw-r--r--src/mailman/chains/reject.py6
-rw-r--r--src/mailman/chains/tests/test_base.py3
-rw-r--r--src/mailman/chains/tests/test_headers.py3
-rw-r--r--src/mailman/chains/tests/test_hold.py6
-rw-r--r--src/mailman/chains/tests/test_owner.py6
-rw-r--r--src/mailman/commands/cli_aliases.py6
-rw-r--r--src/mailman/commands/cli_conf.py6
-rw-r--r--src/mailman/commands/cli_control.py17
-rw-r--r--src/mailman/commands/cli_help.py6
-rw-r--r--src/mailman/commands/cli_import.py12
-rw-r--r--src/mailman/commands/cli_info.py6
-rw-r--r--src/mailman/commands/cli_inject.py12
-rw-r--r--src/mailman/commands/cli_lists.py12
-rw-r--r--src/mailman/commands/cli_members.py14
-rw-r--r--src/mailman/commands/cli_qfile.py16
-rw-r--r--src/mailman/commands/cli_status.py6
-rw-r--r--src/mailman/commands/cli_unshunt.py8
-rw-r--r--src/mailman/commands/cli_version.py6
-rw-r--r--src/mailman/commands/cli_withlist.py9
-rw-r--r--src/mailman/commands/docs/echo.rst2
-rw-r--r--src/mailman/commands/docs/help.rst8
-rw-r--r--src/mailman/commands/docs/info.rst29
-rw-r--r--src/mailman/commands/docs/inject.rst10
-rw-r--r--src/mailman/commands/docs/members.rst5
-rw-r--r--src/mailman/commands/docs/membership.rst18
-rw-r--r--src/mailman/commands/docs/qfile.rst7
-rw-r--r--src/mailman/commands/docs/withlist.rst4
-rw-r--r--src/mailman/commands/eml_confirm.py10
-rw-r--r--src/mailman/commands/eml_echo.py6
-rw-r--r--src/mailman/commands/eml_end.py6
-rw-r--r--src/mailman/commands/eml_help.py6
-rw-r--r--src/mailman/commands/eml_membership.py9
-rw-r--r--src/mailman/commands/tests/test_conf.py5
-rw-r--r--src/mailman/commands/tests/test_confirm.py6
-rw-r--r--src/mailman/commands/tests/test_control.py5
-rw-r--r--src/mailman/commands/tests/test_create.py3
-rw-r--r--src/mailman/commands/tests/test_help.py12
-rw-r--r--src/mailman/config/__init__.py3
-rw-r--r--src/mailman/config/config.py106
-rw-r--r--src/mailman/config/mailman.cfg10
-rw-r--r--src/mailman/config/schema.cfg2
-rw-r--r--src/mailman/config/tests/test_archivers.py3
-rw-r--r--src/mailman/config/tests/test_configuration.py45
-rw-r--r--src/mailman/core/chains.py6
-rw-r--r--src/mailman/core/constants.py8
-rw-r--r--src/mailman/core/docs/runner.rst4
-rw-r--r--src/mailman/core/errors.py3
-rw-r--r--src/mailman/core/i18n.py8
-rw-r--r--src/mailman/core/initialize.py16
-rw-r--r--src/mailman/core/logging.py3
-rw-r--r--src/mailman/core/pipelines.py9
-rw-r--r--src/mailman/core/rules.py6
-rw-r--r--src/mailman/core/runner.py34
-rw-r--r--src/mailman/core/switchboard.py40
-rw-r--r--src/mailman/core/system.py6
-rw-r--r--src/mailman/core/tests/test_pipelines.py14
-rw-r--r--src/mailman/core/tests/test_runner.py7
-rw-r--r--src/mailman/database/alembic/__init__.py3
-rw-r--r--src/mailman/database/alembic/env.py6
-rw-r--r--src/mailman/database/alembic/versions/51b7f92bd06c_initial.py3
-rw-r--r--src/mailman/database/base.py11
-rw-r--r--src/mailman/database/factory.py10
-rw-r--r--src/mailman/database/model.py3
-rw-r--r--src/mailman/database/postgresql.py3
-rw-r--r--src/mailman/database/sqlite.py5
-rw-r--r--src/mailman/database/tests/test_factory.py12
-rw-r--r--src/mailman/database/transaction.py4
-rw-r--r--src/mailman/database/types.py7
-rw-r--r--src/mailman/docs/DEVELOP.rst5
-rw-r--r--src/mailman/docs/INTRODUCTION.rst4
-rw-r--r--src/mailman/docs/NEWS.rst17
-rw-r--r--src/mailman/docs/START.rst29
-rw-r--r--src/mailman/docs/STYLEGUIDE.rst28
-rw-r--r--src/mailman/docs/__init__.py3
-rw-r--r--src/mailman/email/message.py12
-rw-r--r--src/mailman/email/tests/test_message.py11
-rw-r--r--src/mailman/email/validate.py6
-rw-r--r--src/mailman/handlers/acknowledge.py11
-rw-r--r--src/mailman/handlers/after_delivery.py6
-rw-r--r--src/mailman/handlers/avoid_duplicates.py6
-rw-r--r--src/mailman/handlers/cleanse.py6
-rw-r--r--src/mailman/handlers/cleanse_dkim.py6
-rw-r--r--src/mailman/handlers/cook_headers.py131
-rw-r--r--src/mailman/handlers/decorate.py10
-rw-r--r--src/mailman/handlers/docs/acknowledge.rst8
-rw-r--r--src/mailman/handlers/docs/avoid-duplicates.rst12
-rw-r--r--src/mailman/handlers/docs/digests.rst6
-rw-r--r--src/mailman/handlers/docs/file-recips.rst27
-rw-r--r--src/mailman/handlers/docs/filtering.rst22
-rw-r--r--src/mailman/handlers/docs/nntp.rst2
-rw-r--r--src/mailman/handlers/docs/replybot.rst8
-rw-r--r--src/mailman/handlers/docs/rfc-2369.rst2
-rw-r--r--src/mailman/handlers/docs/subject-munging.rst107
-rw-r--r--src/mailman/handlers/docs/tagger.rst24
-rw-r--r--src/mailman/handlers/docs/to-outgoing.rst2
-rw-r--r--src/mailman/handlers/file_recipients.py6
-rw-r--r--src/mailman/handlers/member_recipients.py6
-rw-r--r--src/mailman/handlers/mime_delete.py10
-rw-r--r--src/mailman/handlers/owner_recipients.py6
-rw-r--r--src/mailman/handlers/replybot.py8
-rw-r--r--src/mailman/handlers/rfc_2369.py6
-rw-r--r--src/mailman/handlers/subject_prefix.py184
-rw-r--r--src/mailman/handlers/tagger.py12
-rw-r--r--src/mailman/handlers/tests/test_cook_headers.py7
-rw-r--r--src/mailman/handlers/tests/test_file_recips.py73
-rw-r--r--src/mailman/handlers/tests/test_filter.py57
-rw-r--r--src/mailman/handlers/tests/test_mimedel.py6
-rw-r--r--src/mailman/handlers/tests/test_recipients.py31
-rw-r--r--src/mailman/handlers/tests/test_subject_prefix.py129
-rw-r--r--src/mailman/handlers/tests/test_to_digest.py3
-rw-r--r--src/mailman/handlers/to_archive.py6
-rw-r--r--src/mailman/handlers/to_digest.py10
-rw-r--r--src/mailman/handlers/to_outgoing.py9
-rw-r--r--src/mailman/handlers/to_usenet.py11
-rw-r--r--src/mailman/interfaces/action.py1
-rw-r--r--src/mailman/interfaces/address.py6
-rw-r--r--src/mailman/interfaces/archiver.py3
-rw-r--r--src/mailman/interfaces/autorespond.py4
-rw-r--r--src/mailman/interfaces/bans.py3
-rw-r--r--src/mailman/interfaces/bounce.py3
-rw-r--r--src/mailman/interfaces/chain.py3
-rw-r--r--src/mailman/interfaces/command.py3
-rw-r--r--src/mailman/interfaces/configuration.py6
-rw-r--r--src/mailman/interfaces/database.py3
-rw-r--r--src/mailman/interfaces/digests.py3
-rw-r--r--src/mailman/interfaces/domain.py3
-rw-r--r--src/mailman/interfaces/errors.py3
-rw-r--r--src/mailman/interfaces/handler.py3
-rw-r--r--src/mailman/interfaces/languages.py3
-rw-r--r--src/mailman/interfaces/listmanager.py3
-rw-r--r--src/mailman/interfaces/mailinglist.py6
-rw-r--r--src/mailman/interfaces/member.py6
-rw-r--r--src/mailman/interfaces/messages.py3
-rw-r--r--src/mailman/interfaces/mime.py3
-rw-r--r--src/mailman/interfaces/mlistrequest.py3
-rw-r--r--src/mailman/interfaces/mta.py6
-rw-r--r--src/mailman/interfaces/nntp.py3
-rw-r--r--src/mailman/interfaces/pending.py3
-rw-r--r--src/mailman/interfaces/permissions.py3
-rw-r--r--src/mailman/interfaces/pipeline.py4
-rw-r--r--src/mailman/interfaces/preferences.py3
-rw-r--r--src/mailman/interfaces/registrar.py3
-rw-r--r--src/mailman/interfaces/requests.py3
-rw-r--r--src/mailman/interfaces/roster.py3
-rw-r--r--src/mailman/interfaces/rules.py3
-rw-r--r--src/mailman/interfaces/runner.py3
-rw-r--r--src/mailman/interfaces/styles.py5
-rw-r--r--src/mailman/interfaces/subscriptions.py6
-rw-r--r--src/mailman/interfaces/switchboard.py3
-rw-r--r--src/mailman/interfaces/system.py3
-rw-r--r--src/mailman/interfaces/templates.py3
-rw-r--r--src/mailman/interfaces/user.py6
-rw-r--r--src/mailman/interfaces/usermanager.py3
-rw-r--r--src/mailman/languages/language.py6
-rw-r--r--src/mailman/languages/manager.py8
-rw-r--r--src/mailman/model/address.py12
-rw-r--r--src/mailman/model/autorespond.py10
-rw-r--r--src/mailman/model/bans.py8
-rw-r--r--src/mailman/model/bounce.py8
-rw-r--r--src/mailman/model/digests.py10
-rw-r--r--src/mailman/model/docs/addresses.rst20
-rw-r--r--src/mailman/model/docs/domains.rst14
-rw-r--r--src/mailman/model/docs/languages.rst4
-rw-r--r--src/mailman/model/docs/listmanager.rst16
-rw-r--r--src/mailman/model/docs/mailinglist.rst32
-rw-r--r--src/mailman/model/docs/membership.rst36
-rw-r--r--src/mailman/model/docs/messagestore.rst24
-rw-r--r--src/mailman/model/docs/pending.rst10
-rw-r--r--src/mailman/model/docs/registration.rst35
-rw-r--r--src/mailman/model/docs/usermanager.rst2
-rw-r--r--src/mailman/model/docs/users.rst77
-rw-r--r--src/mailman/model/domain.py12
-rw-r--r--src/mailman/model/language.py8
-rw-r--r--src/mailman/model/listmanager.py8
-rw-r--r--src/mailman/model/mailinglist.py26
-rw-r--r--src/mailman/model/member.py13
-rw-r--r--src/mailman/model/message.py11
-rw-r--r--src/mailman/model/messagestore.py21
-rw-r--r--src/mailman/model/mime.py10
-rw-r--r--src/mailman/model/pending.py52
-rw-r--r--src/mailman/model/preferences.py10
-rw-r--r--src/mailman/model/requests.py24
-rw-r--r--src/mailman/model/roster.py8
-rw-r--r--src/mailman/model/tests/test_address.py25
-rw-r--r--src/mailman/model/tests/test_bounce.py7
-rw-r--r--src/mailman/model/tests/test_domain.py22
-rw-r--r--src/mailman/model/tests/test_listmanager.py31
-rw-r--r--src/mailman/model/tests/test_mailinglist.py45
-rw-r--r--src/mailman/model/tests/test_member.py3
-rw-r--r--src/mailman/model/tests/test_messagestore.py71
-rw-r--r--src/mailman/model/tests/test_registrar.py64
-rw-r--r--src/mailman/model/tests/test_requests.py3
-rw-r--r--src/mailman/model/tests/test_roster.py6
-rw-r--r--src/mailman/model/tests/test_uid.py4
-rw-r--r--src/mailman/model/tests/test_user.py44
-rw-r--r--src/mailman/model/uid.py6
-rw-r--r--src/mailman/model/user.py16
-rw-r--r--src/mailman/model/usermanager.py6
-rw-r--r--src/mailman/mta/aliases.py6
-rw-r--r--src/mailman/mta/base.py6
-rw-r--r--src/mailman/mta/bulk.py4
-rw-r--r--src/mailman/mta/connection.py3
-rw-r--r--src/mailman/mta/decorating.py3
-rw-r--r--src/mailman/mta/deliver.py3
-rw-r--r--src/mailman/mta/docs/authentication.rst2
-rw-r--r--src/mailman/mta/docs/bulk.rst9
-rw-r--r--src/mailman/mta/docs/connection.rst24
-rw-r--r--src/mailman/mta/exim4.py3
-rw-r--r--src/mailman/mta/null.py6
-rw-r--r--src/mailman/mta/personalized.py6
-rw-r--r--src/mailman/mta/postfix.py10
-rw-r--r--src/mailman/mta/tests/test_aliases.py6
-rw-r--r--src/mailman/mta/tests/test_connection.py51
-rw-r--r--src/mailman/mta/tests/test_delivery.py3
-rw-r--r--src/mailman/mta/verp.py3
-rw-r--r--src/mailman/options.py8
-rw-r--r--src/mailman/rest/addresses.py13
-rw-r--r--src/mailman/rest/docs/__init__.py3
-rw-r--r--src/mailman/rest/docs/addresses.rst15
-rw-r--r--src/mailman/rest/docs/basic.rst5
-rw-r--r--src/mailman/rest/docs/domains.rst8
-rw-r--r--src/mailman/rest/docs/helpers.rst17
-rw-r--r--src/mailman/rest/docs/membership.rst4
-rw-r--r--src/mailman/rest/docs/moderation.rst7
-rw-r--r--src/mailman/rest/docs/preferences.rst2
-rw-r--r--src/mailman/rest/docs/queues.rst174
-rw-r--r--src/mailman/rest/docs/users.rst30
-rw-r--r--src/mailman/rest/domains.py13
-rw-r--r--src/mailman/rest/helpers.py11
-rw-r--r--src/mailman/rest/listconf.py36
-rw-r--r--src/mailman/rest/lists.py21
-rw-r--r--src/mailman/rest/members.py20
-rw-r--r--src/mailman/rest/moderation.py5
-rw-r--r--src/mailman/rest/preferences.py3
-rw-r--r--src/mailman/rest/queues.py129
-rw-r--r--src/mailman/rest/root.py30
-rw-r--r--src/mailman/rest/templates.py3
-rw-r--r--src/mailman/rest/tests/test_addresses.py20
-rw-r--r--src/mailman/rest/tests/test_domains.py21
-rw-r--r--src/mailman/rest/tests/test_listconf.py3
-rw-r--r--src/mailman/rest/tests/test_lists.py18
-rw-r--r--src/mailman/rest/tests/test_membership.py18
-rw-r--r--src/mailman/rest/tests/test_moderation.py26
-rw-r--r--src/mailman/rest/tests/test_paginate.py3
-rw-r--r--src/mailman/rest/tests/test_preferences.py6
-rw-r--r--src/mailman/rest/tests/test_queues.py107
-rw-r--r--src/mailman/rest/tests/test_root.py14
-rw-r--r--src/mailman/rest/tests/test_systemconf.py1
-rw-r--r--src/mailman/rest/tests/test_users.py67
-rw-r--r--src/mailman/rest/users.py25
-rw-r--r--src/mailman/rest/validator.py10
-rw-r--r--src/mailman/rest/wsgiapp.py5
-rw-r--r--src/mailman/rules/administrivia.py8
-rw-r--r--src/mailman/rules/any.py6
-rw-r--r--src/mailman/rules/approved.py8
-rw-r--r--src/mailman/rules/emergency.py6
-rw-r--r--src/mailman/rules/implicit_dest.py7
-rw-r--r--src/mailman/rules/loop.py6
-rw-r--r--src/mailman/rules/max_recipients.py6
-rw-r--r--src/mailman/rules/max_size.py6
-rw-r--r--src/mailman/rules/moderation.py8
-rw-r--r--src/mailman/rules/news_moderation.py6
-rw-r--r--src/mailman/rules/no_subject.py6
-rw-r--r--src/mailman/rules/suspicious.py7
-rw-r--r--src/mailman/rules/tests/test_approved.py17
-rw-r--r--src/mailman/rules/tests/test_moderation.py3
-rw-r--r--src/mailman/rules/truth.py6
-rw-r--r--src/mailman/runners/archive.py4
-rw-r--r--src/mailman/runners/bounce.py5
-rw-r--r--src/mailman/runners/command.py34
-rw-r--r--src/mailman/runners/digest.py34
-rw-r--r--src/mailman/runners/docs/command.rst21
-rw-r--r--src/mailman/runners/docs/digester.rst218
-rw-r--r--src/mailman/runners/docs/incoming.rst10
-rw-r--r--src/mailman/runners/docs/lmtp.rst60
-rw-r--r--src/mailman/runners/docs/nntp.rst2
-rw-r--r--src/mailman/runners/docs/outgoing.rst26
-rw-r--r--src/mailman/runners/incoming.py6
-rw-r--r--src/mailman/runners/lmtp.py35
-rw-r--r--src/mailman/runners/nntp.py15
-rw-r--r--src/mailman/runners/outgoing.py10
-rw-r--r--src/mailman/runners/pipeline.py5
-rw-r--r--src/mailman/runners/rest.py3
-rw-r--r--src/mailman/runners/retry.py3
-rw-r--r--src/mailman/runners/tests/test_archiver.py25
-rw-r--r--src/mailman/runners/tests/test_bounce.py16
-rw-r--r--src/mailman/runners/tests/test_confirm.py23
-rw-r--r--src/mailman/runners/tests/test_digest.py83
-rw-r--r--src/mailman/runners/tests/test_incoming.py10
-rw-r--r--src/mailman/runners/tests/test_join.py22
-rw-r--r--src/mailman/runners/tests/test_lmtp.py40
-rw-r--r--src/mailman/runners/tests/test_nntp.py39
-rw-r--r--src/mailman/runners/tests/test_outgoing.py60
-rw-r--r--src/mailman/runners/tests/test_owner.py14
-rw-r--r--src/mailman/runners/tests/test_pipeline.py13
-rw-r--r--src/mailman/runners/tests/test_rest.py3
-rw-r--r--src/mailman/runners/tests/test_retry.py8
-rw-r--r--src/mailman/runners/virgin.py5
-rw-r--r--src/mailman/styles/base.py4
-rw-r--r--src/mailman/styles/default.py6
-rw-r--r--src/mailman/styles/manager.py10
-rw-r--r--src/mailman/styles/tests/test_styles.py10
-rw-r--r--src/mailman/testing/documentation.py9
-rw-r--r--src/mailman/testing/helpers.py27
-rw-r--r--src/mailman/testing/i18n.py6
-rw-r--r--src/mailman/testing/layers.py20
-rw-r--r--src/mailman/testing/mta.py31
-rw-r--r--src/mailman/testing/nose.py10
-rw-r--r--src/mailman/tests/test_configfile.py9
-rw-r--r--src/mailman/utilities/datetime.py4
-rw-r--r--src/mailman/utilities/email.py10
-rw-r--r--src/mailman/utilities/filesystem.py3
-rw-r--r--src/mailman/utilities/i18n.py10
-rw-r--r--src/mailman/utilities/importer.py51
-rw-r--r--src/mailman/utilities/interact.py9
-rw-r--r--src/mailman/utilities/mailbox.py5
-rw-r--r--src/mailman/utilities/modules.py3
-rw-r--r--src/mailman/utilities/passwords.py7
-rw-r--r--src/mailman/utilities/string.py8
-rw-r--r--src/mailman/utilities/tests/test_email.py3
-rw-r--r--src/mailman/utilities/tests/test_import.py95
-rw-r--r--src/mailman/utilities/tests/test_passwords.py3
-rw-r--r--src/mailman/utilities/tests/test_templates.py19
-rw-r--r--src/mailman/utilities/tests/test_wrap.py4
-rw-r--r--src/mailman/utilities/uid.py4
370 files changed, 2698 insertions, 2946 deletions
diff --git a/src/mailman/__init__.py b/src/mailman/__init__.py
index db7befab7..74040d211 100644
--- a/src/mailman/__init__.py
+++ b/src/mailman/__init__.py
@@ -17,13 +17,6 @@
"""The `mailman` package."""
-from __future__ import absolute_import, print_function, unicode_literals
-
-__metaclass__ = type
-__all__ = [
- ]
-
-
import sys
diff --git a/src/mailman/app/bounces.py b/src/mailman/app/bounces.py
index b0a316ad6..ebfe63cff 100644
--- a/src/mailman/app/bounces.py
+++ b/src/mailman/app/bounces.py
@@ -17,9 +17,6 @@
"""Application level bounce handling."""
-from __future__ import absolute_import, print_function, unicode_literals
-
-__metaclass__ = type
__all__ = [
'ProbeVERP',
'StandardVERP',
@@ -36,10 +33,6 @@ import logging
from email.mime.message import MIMEMessage
from email.mime.text import MIMEText
from email.utils import parseaddr
-from string import Template
-from zope.component import getUtility
-from zope.interface import implementer
-
from mailman.config import config
from mailman.core.i18n import _
from mailman.email.message import OwnerNotification, UserNotification
@@ -50,6 +43,10 @@ from mailman.interfaces.subscriptions import ISubscriptionService
from mailman.utilities.email import split_email
from mailman.utilities.i18n import make
from mailman.utilities.string import oneline
+from string import Template
+from zope.component import getUtility
+from zope.interface import implementer
+
log = logging.getLogger('mailman.config')
elog = logging.getLogger('mailman.error')
@@ -71,8 +68,8 @@ def bounce_message(mlist, msg, error=None):
:type error: Exception
"""
# Bounce a message back to the sender, with an error message if provided
- # in the exception argument.
- if msg.sender is None:
+ # in the exception argument. .sender might be None or the empty string.
+ if not msg.sender:
# We can't bounce the message if we don't know who it's supposed to go
# to.
return
diff --git a/src/mailman/app/commands.py b/src/mailman/app/commands.py
index a0f717138..cfa672de5 100644
--- a/src/mailman/app/commands.py
+++ b/src/mailman/app/commands.py
@@ -17,19 +17,15 @@
"""Initialize the email commands."""
-from __future__ import absolute_import, print_function, unicode_literals
-
-__metaclass__ = type
__all__ = [
'initialize',
]
-from zope.interface.verify import verifyObject
-
from mailman.config import config
from mailman.interfaces.command import IEmailCommand
from mailman.utilities.modules import find_components
+from zope.interface.verify import verifyObject
diff --git a/src/mailman/app/docs/hooks.rst b/src/mailman/app/docs/hooks.rst
index eb6cbac05..ba9bb249e 100644
--- a/src/mailman/app/docs/hooks.rst
+++ b/src/mailman/app/docs/hooks.rst
@@ -18,12 +18,12 @@ Hooks name an importable callable so it must be accessible on ``sys.path``.
... counter = 1
... def pre_hook():
... global counter
- ... print 'pre-hook:', counter
+ ... print('pre-hook:', counter)
... counter += 1
...
... def post_hook():
... global counter
- ... print 'post-hook:', counter
+ ... print('post-hook:', counter)
... counter += 1
... """, file=fp)
>>> fp.close()
@@ -61,6 +61,7 @@ script that will produce no output to force the hooks to run.
... proc = subprocess.Popen(
... [exe, 'lists', '--domain', 'ignore', '-q'],
... cwd=ConfigLayer.root_directory, env=env,
+ ... universal_newlines=True,
... stdout=subprocess.PIPE, stderr=subprocess.PIPE)
... stdout, stderr = proc.communicate()
... assert proc.returncode == 0, stderr
diff --git a/src/mailman/app/docs/pipelines.rst b/src/mailman/app/docs/pipelines.rst
index adcdd1ea5..dfdc6d70c 100644
--- a/src/mailman/app/docs/pipelines.rst
+++ b/src/mailman/app/docs/pipelines.rst
@@ -45,9 +45,9 @@ etc.
To: test@example.com
Message-ID: <first>
X-Message-ID-Hash: 4CMWUN6BHVCMHMDAOSJZ2Q72G5M32MWB
- Subject: [Test] My first post
X-Mailman-Version: ...
Precedence: list
+ Subject: [Test] My first post
List-Id: <test.example.com>
Archived-At: http://lists.example.com/.../4CMWUN6BHVCMHMDAOSJZ2Q72G5M32MWB
List-Archive: <http://lists.example.com/archives/test@example.com>
@@ -67,7 +67,7 @@ However there are currently no recipients for this message.
>>> dump_msgdata(msgdata)
original_sender : aperson@example.com
original_subject: My first post
- recipients : set([])
+ recipients : set()
stripped_subject: My first post
After pipeline processing, the message is now sitting in various other
@@ -84,9 +84,9 @@ processing queues.
To: test@example.com
Message-ID: <first>
X-Message-ID-Hash: 4CMWUN6BHVCMHMDAOSJZ2Q72G5M32MWB
- Subject: [Test] My first post
X-Mailman-Version: ...
Precedence: list
+ Subject: [Test] My first post
List-Id: <test.example.com>
...
<BLANKLINE>
@@ -97,7 +97,7 @@ processing queues.
_parsemsg : False
original_sender : aperson@example.com
original_subject: My first post
- recipients : set([])
+ recipients : set()
stripped_subject: My first post
version : 3
@@ -121,9 +121,9 @@ delivered to end recipients.
To: test@example.com
Message-ID: <first>
X-Message-ID-Hash: 4CMWUN6BHVCMHMDAOSJZ2Q72G5M32MWB
- Subject: [Test] My first post
X-Mailman-Version: ...
Precedence: list
+ Subject: [Test] My first post
List-Id: <test.example.com>
...
<BLANKLINE>
@@ -132,10 +132,10 @@ delivered to end recipients.
>>> dump_msgdata(messages[0].msgdata)
_parsemsg : False
- listname : test@example.com
+ listid : test.example.com
original_sender : aperson@example.com
original_subject: My first post
- recipients : set([])
+ recipients : set()
stripped_subject: My first post
version : 3
@@ -152,9 +152,9 @@ There's now one message in the digest mailbox, getting ready to be sent.
To: test@example.com
Message-ID: <first>
X-Message-ID-Hash: 4CMWUN6BHVCMHMDAOSJZ2Q72G5M32MWB
- Subject: [Test] My first post
X-Mailman-Version: ...
Precedence: list
+ Subject: [Test] My first post
List-Id: <test.example.com>
...
<BLANKLINE>
diff --git a/src/mailman/app/docs/subscriptions.rst b/src/mailman/app/docs/subscriptions.rst
index 8c3d8b28d..eaccdc3cc 100644
--- a/src/mailman/app/docs/subscriptions.rst
+++ b/src/mailman/app/docs/subscriptions.rst
@@ -67,13 +67,6 @@ New members can also be added by providing an existing user id instead of an
email address. However, the user must have a preferred email address.
::
- >>> service.join('test.example.com', bart.user.user_id,
- ... role=MemberRole.owner)
- Traceback (most recent call last):
- ...
- MissingPreferredAddressError: User must have a preferred address:
- <User "Bart Person" (2) at ...>
-
>>> from mailman.utilities.datetime import now
>>> address = list(bart.user.addresses)[0]
>>> address.verified_on = now()
diff --git a/src/mailman/app/domain.py b/src/mailman/app/domain.py
index 7ad976699..a8a2cd71a 100644
--- a/src/mailman/app/domain.py
+++ b/src/mailman/app/domain.py
@@ -17,18 +17,14 @@
"""Application level domain support."""
-from __future__ import absolute_import, print_function, unicode_literals
-
-__metaclass__ = type
__all__ = [
'handle_DomainDeletingEvent',
]
-from zope.component import getUtility
-
from mailman.interfaces.domain import DomainDeletingEvent
from mailman.interfaces.listmanager import IListManager
+from zope.component import getUtility
diff --git a/src/mailman/app/events.py b/src/mailman/app/events.py
index 16817c202..0b7f2309e 100644
--- a/src/mailman/app/events.py
+++ b/src/mailman/app/events.py
@@ -17,22 +17,18 @@
"""Global events."""
-from __future__ import absolute_import, print_function, unicode_literals
-
-__metaclass__ = type
__all__ = [
'initialize',
]
-from zope import event
-
from mailman.app import (
domain, membership, moderator, registrar, subscriptions)
from mailman.core import i18n, switchboard
from mailman.languages import manager as language_manager
from mailman.styles import manager as style_manager
from mailman.utilities import passwords
+from zope import event
diff --git a/src/mailman/app/inject.py b/src/mailman/app/inject.py
index 4c182657d..7e8c359ea 100644
--- a/src/mailman/app/inject.py
+++ b/src/mailman/app/inject.py
@@ -17,9 +17,6 @@
"""Inject a message into a queue."""
-from __future__ import absolute_import, print_function, unicode_literals
-
-__metaclass__ = type
__all__ = [
'inject_message',
'inject_text',
@@ -28,7 +25,6 @@ __all__ = [
from email import message_from_string
from email.utils import formatdate, make_msgid
-
from mailman.config import config
from mailman.email.message import Message
from mailman.utilities.email import add_message_hash
@@ -53,6 +49,8 @@ def inject_message(mlist, msg, recipients=None, switchboard=None, **kws):
:type switchboard: string
:param kws: Additional values for the message metadata.
:type kws: dictionary
+ :return: filebase of enqueued message
+ :rtype: string
"""
if switchboard is None:
switchboard = 'in'
@@ -66,13 +64,13 @@ def inject_message(mlist, msg, recipients=None, switchboard=None, **kws):
msg['Date'] = formatdate(localtime=True)
msg.original_size = len(msg.as_string())
msgdata = dict(
- listname=mlist.fqdn_listname,
+ listid=mlist.list_id,
original_size=msg.original_size,
)
msgdata.update(kws)
if recipients is not None:
msgdata['recipients'] = recipients
- config.switchboards[switchboard].enqueue(msg, **msgdata)
+ return config.switchboards[switchboard].enqueue(msg, **msgdata)
@@ -95,6 +93,8 @@ def inject_text(mlist, text, recipients=None, switchboard=None, **kws):
:type switchboard: string
:param kws: Additional values for the message metadata.
:type kws: dictionary
+ :return: filebase of enqueued message
+ :rtype: string
"""
message = message_from_string(text, Message)
- inject_message(mlist, message, recipients, switchboard, **kws)
+ return inject_message(mlist, message, recipients, switchboard, **kws)
diff --git a/src/mailman/app/lifecycle.py b/src/mailman/app/lifecycle.py
index 8110fe69d..bef8320d0 100644
--- a/src/mailman/app/lifecycle.py
+++ b/src/mailman/app/lifecycle.py
@@ -17,9 +17,6 @@
"""Application level list creation."""
-from __future__ import absolute_import, print_function, unicode_literals
-
-__metaclass__ = type
__all__ = [
'create_list',
'remove_list',
@@ -31,8 +28,6 @@ import errno
import shutil
import logging
-from zope.component import getUtility
-
from mailman.config import config
from mailman.interfaces.address import IEmailValidator
from mailman.interfaces.domain import (
@@ -42,6 +37,7 @@ from mailman.interfaces.member import MemberRole
from mailman.interfaces.styles import IStyleManager
from mailman.interfaces.usermanager import IUserManager
from mailman.utilities.modules import call_name
+from zope.component import getUtility
log = logging.getLogger('mailman.error')
diff --git a/src/mailman/app/membership.py b/src/mailman/app/membership.py
index 4ec6b7878..0a6c8b971 100644
--- a/src/mailman/app/membership.py
+++ b/src/mailman/app/membership.py
@@ -17,9 +17,6 @@
"""Application support for membership management."""
-from __future__ import absolute_import, print_function, unicode_literals
-
-__metaclass__ = type
__all__ = [
'add_member',
'delete_member',
@@ -28,8 +25,6 @@ __all__ = [
from email.utils import formataddr
-from zope.component import getUtility
-
from mailman.app.notifications import (
send_goodbye_message, send_welcome_message)
from mailman.config import config
@@ -40,6 +35,7 @@ from mailman.interfaces.member import (
MemberRole, MembershipIsBannedError, NotAMemberError, SubscriptionEvent)
from mailman.interfaces.usermanager import IUserManager
from mailman.utilities.i18n import make
+from zope.component import getUtility
diff --git a/src/mailman/app/moderator.py b/src/mailman/app/moderator.py
index 105e53617..d4c5b1036 100644
--- a/src/mailman/app/moderator.py
+++ b/src/mailman/app/moderator.py
@@ -17,9 +17,6 @@
"""Application support for moderators."""
-from __future__ import absolute_import, print_function, unicode_literals
-
-__metaclass__ = type
__all__ = [
'handle_ListDeletingEvent',
'handle_message',
@@ -35,8 +32,6 @@ import time
import logging
from email.utils import formataddr, formatdate, getaddresses, make_msgid
-from zope.component import getUtility
-
from mailman.app.membership import add_member, delete_member
from mailman.app.notifications import send_admin_subscription_notice
from mailman.config import config
@@ -51,6 +46,7 @@ from mailman.interfaces.messages import IMessageStore
from mailman.interfaces.requests import IListRequests, RequestType
from mailman.utilities.datetime import now
from mailman.utilities.i18n import make
+from zope.component import getUtility
NL = '\n'
@@ -86,14 +82,14 @@ def hold_message(mlist, msg, msgdata=None, reason=None):
# Message-ID header.
message_id = msg.get('message-id')
if message_id is None:
- msg['Message-ID'] = message_id = make_msgid().decode('ascii')
+ msg['Message-ID'] = message_id = make_msgid()
elif isinstance(message_id, bytes):
message_id = message_id.decode('ascii')
getUtility(IMessageStore).add(msg)
# Prepare the message metadata with some extra information needed only by
# the moderation interface.
msgdata['_mod_message_id'] = message_id
- msgdata['_mod_fqdn_listname'] = mlist.fqdn_listname
+ msgdata['_mod_listid'] = mlist.list_id
msgdata['_mod_sender'] = msg.sender
msgdata['_mod_subject'] = msg.get('subject', _('(no subject)'))
msgdata['_mod_reason'] = reason
@@ -134,7 +130,7 @@ def handle_message(mlist, id, action,
# Start by getting the message from the message store.
msg = message_store.get_message_by_id(message_id)
# Delete moderation-specific entries from the message metadata.
- for key in msgdata.keys():
+ for key in list(msgdata):
if key.startswith('_mod_'):
del msgdata[key]
# Add some metadata to indicate this message has now been approved.
diff --git a/src/mailman/app/notifications.py b/src/mailman/app/notifications.py
index 1fa1fe01e..163b02653 100644
--- a/src/mailman/app/notifications.py
+++ b/src/mailman/app/notifications.py
@@ -17,9 +17,6 @@
"""Sending notifications."""
-from __future__ import absolute_import, print_function, unicode_literals
-
-__metaclass__ = type
__all__ = [
'send_admin_subscription_notice',
'send_goodbye_message',
@@ -31,9 +28,6 @@ import logging
from email.utils import formataddr
from lazr.config import as_boolean
-from urllib2 import URLError
-from zope.component import getUtility
-
from mailman.config import config
from mailman.core.i18n import _
from mailman.email.message import OwnerNotification, UserNotification
@@ -41,6 +35,8 @@ from mailman.interfaces.member import DeliveryMode
from mailman.interfaces.templates import ITemplateLoader
from mailman.utilities.i18n import make
from mailman.utilities.string import expand, wrap
+from six.moves.urllib_error import URLError
+from zope.component import getUtility
log = logging.getLogger('mailman.error')
@@ -141,7 +137,6 @@ def send_admin_subscription_notice(mlist, address, display_name, language):
"""
with _.using(mlist.preferred_language.code):
subject = _('$mlist.display_name subscription notification')
- display_name = display_name.encode(language.charset, 'replace')
text = make('adminsubscribeack.txt',
mailing_list=mlist,
listname=mlist.display_name,
diff --git a/src/mailman/app/registrar.py b/src/mailman/app/registrar.py
index aa4e35483..fd84f7aa0 100644
--- a/src/mailman/app/registrar.py
+++ b/src/mailman/app/registrar.py
@@ -17,9 +17,6 @@
"""Implementation of the IUserRegistrar interface."""
-from __future__ import absolute_import, print_function, unicode_literals
-
-__metaclass__ = type
__all__ = [
'Registrar',
'handle_ConfirmationNeededEvent',
@@ -28,10 +25,6 @@ __all__ = [
import logging
-from zope.component import getUtility
-from zope.event import notify
-from zope.interface import implementer
-
from mailman.core.i18n import _
from mailman.email.message import UserNotification
from mailman.interfaces.address import IEmailValidator
@@ -42,6 +35,9 @@ from mailman.interfaces.registrar import ConfirmationNeededEvent, IRegistrar
from mailman.interfaces.templates import ITemplateLoader
from mailman.interfaces.usermanager import IUserManager
from mailman.utilities.datetime import now
+from zope.component import getUtility
+from zope.event import notify
+from zope.interface import implementer
log = logging.getLogger('mailman.error')
diff --git a/src/mailman/app/replybot.py b/src/mailman/app/replybot.py
index 4ade73faf..ca563ea0a 100644
--- a/src/mailman/app/replybot.py
+++ b/src/mailman/app/replybot.py
@@ -21,9 +21,6 @@
# mailing list. The reply governor should really apply site-wide per
# recipient (I think).
-from __future__ import absolute_import, print_function, unicode_literals
-
-__metaclass__ = type
__all__ = [
'can_acknowledge',
]
diff --git a/src/mailman/app/subscriptions.py b/src/mailman/app/subscriptions.py
index 99c6ab2de..e3239e97e 100644
--- a/src/mailman/app/subscriptions.py
+++ b/src/mailman/app/subscriptions.py
@@ -15,11 +15,8 @@
# You should have received a copy of the GNU General Public License along with
# GNU Mailman. If not, see <http://www.gnu.org/licenses/>.
-"""Module stuff."""
+"""Handle subscriptions."""
-from __future__ import absolute_import, print_function, unicode_literals
-
-__metaclass__ = type
__all__ = [
'SubscriptionService',
'handle_ListDeletingEvent',
@@ -108,7 +105,7 @@ class SubscriptionService:
# the parameter can either be an email address or a user id.
query = []
if subscriber is not None:
- if isinstance(subscriber, basestring):
+ if isinstance(subscriber, str):
# subscriber is an email address.
address = user_manager.get_address(subscriber)
user = user_manager.get_user(subscriber)
@@ -148,7 +145,7 @@ class SubscriptionService:
if mlist is None:
raise NoSuchListError(list_id)
# Is the subscriber an email address or user id?
- if isinstance(subscriber, basestring):
+ if isinstance(subscriber, str):
if display_name is None:
display_name, at, domain = subscriber.partition('@')
# Because we want to keep the REST API simple, there is no
diff --git a/src/mailman/app/templates.py b/src/mailman/app/templates.py
index 742584b49..a5f9fc1b5 100644
--- a/src/mailman/app/templates.py
+++ b/src/mailman/app/templates.py
@@ -17,30 +17,27 @@
"""Template loader."""
-from __future__ import absolute_import, print_function, unicode_literals
-
-__metaclass__ = type
__all__ = [
'TemplateLoader',
]
-import urllib2
-
from contextlib import closing
-from urllib import addinfourl
-from urlparse import urlparse
-from zope.component import getUtility
-from zope.interface import implementer
-
-from mailman.utilities.i18n import TemplateNotFoundError, find
from mailman.interfaces.languages import ILanguageManager
from mailman.interfaces.listmanager import IListManager
from mailman.interfaces.templates import ITemplateLoader
+from mailman.utilities.i18n import TemplateNotFoundError, find
+from six.moves.urllib_error import URLError
+from six.moves.urllib_parse import urlparse
+from six.moves.urllib_request import (
+ BaseHandler, build_opener, install_opener, urlopen)
+from six.moves.urllib_response import addinfourl
+from zope.component import getUtility
+from zope.interface import implementer
-class MailmanHandler(urllib2.BaseHandler):
+class MailmanHandler(BaseHandler):
# Handle internal mailman: URLs.
def mailman_open(self, req):
# Parse urls of the form:
@@ -55,9 +52,9 @@ class MailmanHandler(urllib2.BaseHandler):
assert parsed.scheme == 'mailman'
# The path can contain one, two, or three components. Since no empty
# path components are legal, filter them out.
- parts = filter(None, parsed.path.split('/'))
+ parts = [p for p in parsed.path.split('/') if p]
if len(parts) == 0:
- raise urllib2.URLError('No template specified')
+ raise URLError('No template specified')
elif len(parts) == 1:
template = parts[0]
elif len(parts) == 2:
@@ -69,25 +66,25 @@ class MailmanHandler(urllib2.BaseHandler):
language = getUtility(ILanguageManager).get(part0)
mlist = getUtility(IListManager).get(part0)
if language is None and mlist is None:
- raise urllib2.URLError('Bad language or list name')
+ raise URLError('Bad language or list name')
elif mlist is None:
code = language.code
elif len(parts) == 3:
fqdn_listname, code, template = parts
mlist = getUtility(IListManager).get(fqdn_listname)
if mlist is None:
- raise urllib2.URLError('Missing list')
+ raise URLError('Missing list')
language = getUtility(ILanguageManager).get(code)
if language is None:
- raise urllib2.URLError('No such language')
+ raise URLError('No such language')
code = language.code
else:
- raise urllib2.URLError('No such file')
+ raise URLError('No such file')
# Find the template, mutating any missing template exception.
try:
path, fp = find(template, mlist, code)
except TemplateNotFoundError:
- raise urllib2.URLError('No such file')
+ raise URLError('No such file')
return addinfourl(fp, {}, original_url)
@@ -97,10 +94,10 @@ class TemplateLoader:
"""Loader of templates, with caching and support for mailman:// URIs."""
def __init__(self):
- opener = urllib2.build_opener(MailmanHandler())
- urllib2.install_opener(opener)
+ opener = build_opener(MailmanHandler())
+ install_opener(opener)
def get(self, uri):
"""See `ITemplateLoader`."""
- with closing(urllib2.urlopen(uri)) as fp:
- return fp.read().decode('utf-8')
+ with closing(urlopen(uri)) as fp:
+ return fp.read()
diff --git a/src/mailman/app/tests/test_bounces.py b/src/mailman/app/tests/test_bounces.py
index 5eb518786..b89664209 100644
--- a/src/mailman/app/tests/test_bounces.py
+++ b/src/mailman/app/tests/test_bounces.py
@@ -17,9 +17,6 @@
"""Testing app.bounces functions."""
-from __future__ import absolute_import, print_function, unicode_literals
-
-__metaclass__ = type
__all__ = [
'TestBounceMessage',
'TestMaybeForward',
@@ -36,8 +33,6 @@ import shutil
import tempfile
import unittest
-from zope.component import getUtility
-
from mailman.app.bounces import (
ProbeVERP, StandardVERP, bounce_message, maybe_forward, send_probe)
from mailman.app.lifecycle import create_list
@@ -49,10 +44,9 @@ from mailman.interfaces.member import DeliveryMode, MemberRole
from mailman.interfaces.pending import IPendings
from mailman.interfaces.usermanager import IUserManager
from mailman.testing.helpers import (
- LogFileMark,
- get_queue_messages,
- specialized_message_from_string as mfs)
+ LogFileMark, get_queue_messages, specialized_message_from_string as mfs)
from mailman.testing.layers import ConfigLayer
+from zope.component import getUtility
@@ -334,7 +328,7 @@ $owneraddr
send_probe(self._member, self._msg)
message = get_queue_messages('virgin')[0].msg
self.assertEqual(
- message['Subject'],
+ message['subject'].encode(),
'=?utf-8?q?ailing-may_ist-lay_Test_obe-pray_essage-may?=')
def test_probe_notice_with_member_nonenglish(self):
@@ -533,7 +527,7 @@ Subject: Ignore
def test_no_sender(self):
# The message won't be bounced if it has no discernible sender.
- self._msg.sender = None
+ del self._msg['from']
bounce_message(self._mlist, self._msg)
items = get_queue_messages('virgin')
# Nothing in the virgin queue means nothing's been bounced.
diff --git a/src/mailman/app/tests/test_inject.py b/src/mailman/app/tests/test_inject.py
index f7f750662..196c32182 100644
--- a/src/mailman/app/tests/test_inject.py
+++ b/src/mailman/app/tests/test_inject.py
@@ -17,10 +17,9 @@
"""Testing app.inject functions."""
-from __future__ import absolute_import, print_function, unicode_literals
-
-__metaclass__ = type
__all__ = [
+ 'TestInjectMessage',
+ 'TestInjectText',
]
@@ -64,7 +63,7 @@ Nothing.
self.assertEqual(len(items), 1)
self.assertMultiLineEqual(items[0].msg.as_string(),
self.msg.as_string())
- self.assertEqual(items[0].msgdata['listname'], 'test@example.com')
+ self.assertEqual(items[0].msgdata['listid'], 'test.example.com')
self.assertEqual(items[0].msgdata['original_size'],
len(self.msg.as_string()))
@@ -84,7 +83,7 @@ Nothing.
self.assertEqual(len(items), 1)
self.assertMultiLineEqual(items[0].msg.as_string(),
self.msg.as_string())
- self.assertEqual(items[0].msgdata['listname'], 'test@example.com')
+ self.assertEqual(items[0].msgdata['listid'], 'test.example.com')
self.assertEqual(items[0].msgdata['original_size'],
len(self.msg.as_string()))
@@ -144,7 +143,7 @@ class TestInjectText(unittest.TestCase):
def setUp(self):
self.mlist = create_list('test@example.com')
- self.text = b"""\
+ self.text = """\
From: bart@example.com
To: test@example.com
Subject: A test message
@@ -171,7 +170,7 @@ Nothing.
# Delete that header because it is not in the original text.
del items[0].msg['x-message-id-hash']
self.assertMultiLineEqual(items[0].msg.as_string(), self.text)
- self.assertEqual(items[0].msgdata['listname'], 'test@example.com')
+ self.assertEqual(items[0].msgdata['listid'], 'test.example.com')
self.assertEqual(items[0].msgdata['original_size'],
# Add back the X-Message-ID-Header which was in the
# message contributing to the original_size, but
@@ -196,7 +195,7 @@ Nothing.
# Remove the X-Message-ID-Hash header which isn't in the original text.
del items[0].msg['x-message-id-hash']
self.assertMultiLineEqual(items[0].msg.as_string(), self.text)
- self.assertEqual(items[0].msgdata['listname'], 'test@example.com')
+ self.assertEqual(items[0].msgdata['listid'], 'test.example.com')
self.assertEqual(items[0].msgdata['original_size'],
# Add back the X-Message-ID-Header which was in the
# message contributing to the original_size, but
diff --git a/src/mailman/app/tests/test_lifecycle.py b/src/mailman/app/tests/test_lifecycle.py
index 0fb54f193..75386b870 100644
--- a/src/mailman/app/tests/test_lifecycle.py
+++ b/src/mailman/app/tests/test_lifecycle.py
@@ -17,9 +17,6 @@
"""Test the high level list lifecycle API."""
-from __future__ import absolute_import, print_function, unicode_literals
-
-__metaclass__ = type
__all__ = [
'TestLifecycle',
]
diff --git a/src/mailman/app/tests/test_membership.py b/src/mailman/app/tests/test_membership.py
index 95e8de1d0..5b2caf103 100644
--- a/src/mailman/app/tests/test_membership.py
+++ b/src/mailman/app/tests/test_membership.py
@@ -17,9 +17,6 @@
"""Tests of application level membership functions."""
-from __future__ import absolute_import, print_function, unicode_literals
-
-__metaclass__ = type
__all__ = [
'TestAddMember',
'TestAddMemberPassword',
@@ -29,8 +26,6 @@ __all__ = [
import unittest
-from zope.component import getUtility
-
from mailman.app.lifecycle import create_list
from mailman.app.membership import add_member, delete_member
from mailman.core.constants import system_preferences
@@ -40,6 +35,7 @@ from mailman.interfaces.member import (
NotAMemberError)
from mailman.interfaces.usermanager import IUserManager
from mailman.testing.layers import ConfigLayer
+from zope.component import getUtility
diff --git a/src/mailman/app/tests/test_moderation.py b/src/mailman/app/tests/test_moderation.py
index edb6b8c28..190b670d8 100644
--- a/src/mailman/app/tests/test_moderation.py
+++ b/src/mailman/app/tests/test_moderation.py
@@ -17,9 +17,6 @@
"""Moderation tests."""
-from __future__ import absolute_import, print_function, unicode_literals
-
-__metaclass__ = type
__all__ = [
'TestModeration',
]
@@ -27,8 +24,6 @@ __all__ = [
import unittest
-from zope.component import getUtility
-
from mailman.app.lifecycle import create_list
from mailman.app.moderator import handle_message, hold_message
from mailman.interfaces.action import Action
@@ -41,6 +36,7 @@ from mailman.testing.helpers import (
get_queue_messages, make_testable_runner, specialized_message_from_string)
from mailman.testing.layers import SMTPLayer
from mailman.utilities.datetime import now
+from zope.component import getUtility
diff --git a/src/mailman/app/tests/test_notifications.py b/src/mailman/app/tests/test_notifications.py
index 4cdc1c01c..fda4aaa0b 100644
--- a/src/mailman/app/tests/test_notifications.py
+++ b/src/mailman/app/tests/test_notifications.py
@@ -17,10 +17,8 @@
"""Test notifications."""
-from __future__ import absolute_import, print_function, unicode_literals
-
-__metaclass__ = type
__all__ = [
+ 'TestNotifications',
]
@@ -29,8 +27,6 @@ import shutil
import tempfile
import unittest
-from zope.component import getUtility
-
from mailman.app.lifecycle import create_list
from mailman.app.membership import add_member
from mailman.config import config
@@ -38,6 +34,7 @@ from mailman.interfaces.languages import ILanguageManager
from mailman.interfaces.member import DeliveryMode, MemberRole
from mailman.testing.helpers import get_queue_messages
from mailman.testing.layers import ConfigLayer
+from zope.component import getUtility
diff --git a/src/mailman/app/tests/test_registration.py b/src/mailman/app/tests/test_registration.py
index ff128ae6f..fa34005c8 100644
--- a/src/mailman/app/tests/test_registration.py
+++ b/src/mailman/app/tests/test_registration.py
@@ -1,4 +1,4 @@
-# Copyright (C) 2012 by the Free Software Foundation, Inc.
+# Copyright (C) 2012-2014 by the Free Software Foundation, Inc.
#
# This file is part of GNU Mailman.
#
@@ -17,9 +17,6 @@
"""Test email address registration."""
-from __future__ import absolute_import, print_function, unicode_literals
-
-__metaclass__ = type
__all__ = [
'TestEmailValidation',
'TestRegistration',
@@ -28,14 +25,13 @@ __all__ = [
import unittest
-from zope.component import getUtility
-
from mailman.app.lifecycle import create_list
from mailman.interfaces.address import InvalidEmailAddressError
from mailman.interfaces.pending import IPendings
from mailman.interfaces.registrar import ConfirmationNeededEvent, IRegistrar
from mailman.testing.helpers import event_subscribers
from mailman.testing.layers import ConfigLayer
+from zope.component import getUtility
diff --git a/src/mailman/app/tests/test_subscriptions.py b/src/mailman/app/tests/test_subscriptions.py
index e5aad18bc..1ba3cc24b 100644
--- a/src/mailman/app/tests/test_subscriptions.py
+++ b/src/mailman/app/tests/test_subscriptions.py
@@ -17,9 +17,6 @@
"""Tests for the subscription service."""
-from __future__ import absolute_import, print_function, unicode_literals
-
-__metaclass__ = type
__all__ = [
'TestJoin'
]
@@ -28,13 +25,13 @@ __all__ = [
import uuid
import unittest
-from zope.component import getUtility
-
from mailman.app.lifecycle import create_list
from mailman.interfaces.address import InvalidEmailAddressError
+from mailman.interfaces.member import MemberRole, MissingPreferredAddressError
from mailman.interfaces.subscriptions import (
MissingUserError, ISubscriptionService)
from mailman.testing.layers import ConfigLayer
+from zope.component import getUtility
@@ -57,3 +54,14 @@ class TestJoin(unittest.TestCase):
with self.assertRaises(InvalidEmailAddressError) as cm:
self._service.join('test.example.com', 'bogus')
self.assertEqual(cm.exception.email, 'bogus')
+
+ def test_missing_preferred_address(self):
+ # A user cannot join a mailing list if they have no preferred address.
+ anne = self._service.join(
+ 'test.example.com', 'anne@example.com', 'Anne Person')
+ # Try to join Anne as a user with a different role. Her user has no
+ # preferred address, so this will fail.
+ self.assertRaises(MissingPreferredAddressError,
+ self._service.join,
+ 'test.example.com', anne.user.user_id,
+ role=MemberRole.owner)
diff --git a/src/mailman/app/tests/test_templates.py b/src/mailman/app/tests/test_templates.py
index afde68647..68bab9f49 100644
--- a/src/mailman/app/tests/test_templates.py
+++ b/src/mailman/app/tests/test_templates.py
@@ -17,27 +17,24 @@
"""Test the template downloader API."""
-from __future__ import absolute_import, print_function, unicode_literals
-
-__metaclass__ = type
__all__ = [
'TestTemplateLoader',
]
import os
+import six
import shutil
-import urllib2
import tempfile
import unittest
-from zope.component import getUtility
-
from mailman.app.lifecycle import create_list
from mailman.config import config
from mailman.interfaces.languages import ILanguageManager
from mailman.interfaces.templates import ITemplateLoader
from mailman.testing.layers import ConfigLayer
+from six.moves.urllib_error import URLError
+from zope.component import getUtility
@@ -98,32 +95,32 @@ class TestTemplateLoader(unittest.TestCase):
self.assertEqual(content, 'Test content')
def test_uri_not_found(self):
- with self.assertRaises(urllib2.URLError) as cm:
+ with self.assertRaises(URLError) as cm:
self._loader.get('mailman:///missing.txt')
self.assertEqual(cm.exception.reason, 'No such file')
def test_shorter_url_error(self):
- with self.assertRaises(urllib2.URLError) as cm:
+ with self.assertRaises(URLError) as cm:
self._loader.get('mailman:///')
self.assertEqual(cm.exception.reason, 'No template specified')
def test_short_url_error(self):
- with self.assertRaises(urllib2.URLError) as cm:
+ with self.assertRaises(URLError) as cm:
self._loader.get('mailman://')
self.assertEqual(cm.exception.reason, 'No template specified')
def test_bad_language(self):
- with self.assertRaises(urllib2.URLError) as cm:
+ with self.assertRaises(URLError) as cm:
self._loader.get('mailman:///xx/demo.txt')
self.assertEqual(cm.exception.reason, 'Bad language or list name')
def test_bad_mailing_list(self):
- with self.assertRaises(urllib2.URLError) as cm:
+ with self.assertRaises(URLError) as cm:
self._loader.get('mailman:///missing@example.com/demo.txt')
self.assertEqual(cm.exception.reason, 'Bad language or list name')
def test_too_many_path_components(self):
- with self.assertRaises(urllib2.URLError) as cm:
+ with self.assertRaises(URLError) as cm:
self._loader.get('mailman:///missing@example.com/en/foo/demo.txt')
self.assertEqual(cm.exception.reason, 'No such file')
@@ -132,8 +129,8 @@ class TestTemplateLoader(unittest.TestCase):
test_text = b'\xe4\xb8\xad'
path = os.path.join(self.var_dir, 'templates', 'site', 'it')
os.makedirs(path)
- with open(os.path.join(path, 'demo.txt'), 'w') as fp:
- print(test_text, end='', file=fp)
+ with open(os.path.join(path, 'demo.txt'), 'wb') as fp:
+ fp.write(test_text)
content = self._loader.get('mailman:///it/demo.txt')
- self.assertTrue(isinstance(content, unicode))
+ self.assertIsInstance(content, six.text_type)
self.assertEqual(content, test_text.decode('utf-8'))
diff --git a/src/mailman/archiving/mailarchive.py b/src/mailman/archiving/mailarchive.py
index c5fe5d0cb..a712e4052 100644
--- a/src/mailman/archiving/mailarchive.py
+++ b/src/mailman/archiving/mailarchive.py
@@ -17,21 +17,16 @@
"""The Mail-Archive.com archiver."""
-from __future__ import absolute_import, print_function, unicode_literals
-
-__metaclass__ = type
__all__ = [
'MailArchive',
]
-from urllib import quote
-from urlparse import urljoin
-from zope.interface import implementer
-
from mailman.config import config
from mailman.config.config import external_configuration
from mailman.interfaces.archiver import ArchivePolicy, IArchiver
+from six.moves.urllib_parse import quote, urljoin
+from zope.interface import implementer
@@ -77,5 +72,5 @@ class MailArchive:
if mlist.archive_policy is ArchivePolicy.public:
config.switchboards['out'].enqueue(
msg,
- listname=mlist.fqdn_listname,
+ listid=mlist.list_id,
recipients=[self.recipient])
diff --git a/src/mailman/archiving/mhonarc.py b/src/mailman/archiving/mhonarc.py
index f2d1f77fe..31853183f 100644
--- a/src/mailman/archiving/mhonarc.py
+++ b/src/mailman/archiving/mhonarc.py
@@ -17,9 +17,6 @@
"""MHonArc archiver."""
-from __future__ import absolute_import, print_function, unicode_literals
-
-__metaclass__ = type
__all__ = [
'MHonArc',
]
@@ -28,13 +25,12 @@ __all__ = [
import logging
import subprocess
-from urlparse import urljoin
-from zope.interface import implementer
-
from mailman.config import config
from mailman.config.config import external_configuration
from mailman.interfaces.archiver import IArchiver
from mailman.utilities.string import expand
+from six.moves.urllib_parse import urljoin
+from zope.interface import implementer
log = logging.getLogger('mailman.archiver')
@@ -84,7 +80,7 @@ class MHonArc:
command = expand(self.command, substitutions)
proc = subprocess.Popen(
command, stdout=subprocess.PIPE, stderr=subprocess.PIPE,
- shell=True)
+ universal_newlines=True, shell=True)
stdout, stderr = proc.communicate(msg.as_string())
if proc.returncode != 0:
log.error('%s: mhonarc subprocess had non-zero exit code: %s' %
diff --git a/src/mailman/archiving/prototype.py b/src/mailman/archiving/prototype.py
index 77b2294ed..a27a2e57f 100644
--- a/src/mailman/archiving/prototype.py
+++ b/src/mailman/archiving/prototype.py
@@ -17,9 +17,6 @@
"""Prototypical permalinking archiver."""
-from __future__ import absolute_import, print_function, unicode_literals
-
-__metaclass__ = type
__all__ = [
'Prototype',
]
@@ -30,14 +27,13 @@ import errno
import logging
from datetime import timedelta
-from mailbox import Maildir
-from urlparse import urljoin
-
from flufl.lock import Lock, TimeOutError
-from zope.interface import implementer
-
+from mailbox import Maildir
from mailman.config import config
from mailman.interfaces.archiver import IArchiver
+from six.moves.urllib_parse import urljoin
+from zope.interface import implementer
+
log = logging.getLogger('mailman.error')
diff --git a/src/mailman/archiving/tests/test_prototype.py b/src/mailman/archiving/tests/test_prototype.py
index fba46ea4b..4cd33d431 100644
--- a/src/mailman/archiving/tests/test_prototype.py
+++ b/src/mailman/archiving/tests/test_prototype.py
@@ -17,9 +17,6 @@
"""Test the prototype archiver."""
-from __future__ import absolute_import, print_function, unicode_literals
-
-__metaclass__ = type
__all__ = [
'TestPrototypeArchiver',
]
@@ -33,7 +30,6 @@ import threading
from email import message_from_file
from flufl.lock import Lock
-
from mailman.app.lifecycle import create_list
from mailman.archiving.prototype import Prototype
from mailman.config import config
@@ -89,13 +85,13 @@ but the water deserves to be swum.
def _find(self, path):
all_filenames = set()
for dirpath, dirnames, filenames in os.walk(path):
- if not isinstance(dirpath, unicode):
- dirpath = unicode(dirpath)
+ if isinstance(dirpath, bytes):
+ dirpath = dirpath.decode('utf-8')
all_filenames.add(dirpath)
for filename in filenames:
new_filename = filename
- if not isinstance(filename, unicode):
- new_filename = unicode(filename)
+ if isinstance(filename, bytes):
+ new_filename = filename.decode('utf-8')
all_filenames.add(os.path.join(dirpath, new_filename))
return all_filenames
diff --git a/src/mailman/bin/export.py b/src/mailman/bin/export.py
index a5400a9bc..1ee9f31e1 100644
--- a/src/mailman/bin/export.py
+++ b/src/mailman/bin/export.py
@@ -134,7 +134,7 @@ class XMLDumper(object):
print >> self._fp, '<%s%s/>' % (_name, attrs)
else:
# The value might contain angle brackets.
- value = escape(unicode(_value))
+ value = escape(_value.decode('utf-8'))
print >> self._fp, '<%s%s>%s</%s>' % (_name, attrs, value, _name)
def _do_list_categories(self, mlist, k, subcat=None):
diff --git a/src/mailman/bin/gate_news.py b/src/mailman/bin/gate_news.py
index 9bb1e5f61..275956fc1 100644
--- a/src/mailman/bin/gate_news.py
+++ b/src/mailman/bin/gate_news.py
@@ -149,7 +149,7 @@ def poll_newsgroup(mlist, conn, first, last, glock):
# Post the message to the locked list
inq = Switchboard(config.INQUEUE_DIR)
inq.enqueue(msg,
- listname=mlist.internal_name(),
+ listid=mlist.list_id,
fromusenet=True)
log.info('posted to list %s: %7d', listname, num)
except nntplib.NNTPError as e:
diff --git a/src/mailman/bin/mailman.py b/src/mailman/bin/mailman.py
index 67f4d0910..ad8de144f 100644
--- a/src/mailman/bin/mailman.py
+++ b/src/mailman/bin/mailman.py
@@ -17,9 +17,6 @@
"""The 'mailman' command dispatcher."""
-from __future__ import absolute_import, print_function, unicode_literals
-
-__metaclass__ = type
__all__ = [
'main',
]
@@ -28,13 +25,13 @@ __all__ = [
import os
import argparse
-from zope.interface.verify import verifyObject
-
+from functools import cmp_to_key
from mailman.core.i18n import _
from mailman.core.initialize import initialize
from mailman.interfaces.command import ICLISubCommand
from mailman.utilities.modules import find_components
from mailman.version import MAILMAN_VERSION_FULL
+from zope.interface.verify import verifyObject
@@ -77,9 +74,14 @@ def main():
return -1
elif other.name == 'help':
return 1
+ elif command.name < other.name:
+ return -1
+ elif command.name == other.name:
+ return 0
else:
- return cmp(command.name, other.name)
- subcommands.sort(cmp=sort_function)
+ assert command.name > other.name
+ return 1
+ subcommands.sort(key=cmp_to_key(sort_function))
for command in subcommands:
command_parser = subparser.add_parser(
command.name, help=_(command.__doc__))
diff --git a/src/mailman/bin/master.py b/src/mailman/bin/master.py
index 50e8cb5bf..e7efdc537 100644
--- a/src/mailman/bin/master.py
+++ b/src/mailman/bin/master.py
@@ -17,9 +17,6 @@
"""Master subprocess watcher."""
-from __future__ import absolute_import, print_function, unicode_literals
-
-__metaclass__ = type
__all__ = [
'Loop',
'main',
@@ -37,7 +34,6 @@ from datetime import timedelta
from enum import Enum
from flufl.lock import Lock, NotLockedError, TimeOutError
from lazr.config import as_boolean
-
from mailman.config import config
from mailman.core.i18n import _
from mailman.core.logging import reopen
@@ -357,7 +353,7 @@ class Loop:
# Set the environment variable which tells the runner that it's
# running under bin/master control. This subtly changes the error
# behavior of bin/runner.
- os.environ['MAILMAN_UNDER_MASTER_CONTROL'] = '1'
+ env = {'MAILMAN_UNDER_MASTER_CONTROL': '1'}
# Craft the command line arguments for the exec() call.
rswitch = '--runner=' + spec
# Wherever master lives, so too must live the runner script.
@@ -365,15 +361,21 @@ class Loop:
# 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]
+ # Always pass the explicit path to the configuration file to the
+ # sub-runners. This avoids any debate about which cfg file is used.
config_file = (config.filename if self._config_file is None
else self._config_file)
args.extend(['-C', config_file])
log = logging.getLogger('mailman.runner')
log.debug('starting: %s', args)
+ # We must pass this environment variable through if it's set,
+ # otherwise runner processes will not have the correct VAR_DIR.
+ var_dir = os.environ.get('MAILMAN_VAR_DIR')
+ if var_dir is not None:
+ env['MAILMAN_VAR_DIR'] = var_dir
# For the testing framework, if this environment variable is set, pass
# it on to the subprocess.
coverage_env = os.environ.get('COVERAGE_PROCESS_START')
- env = dict()
if coverage_env is not None:
env['COVERAGE_PROCESS_START'] = coverage_env
args.append(env)
diff --git a/src/mailman/bin/onebounce.py b/src/mailman/bin/onebounce.py
index 1c23fc42a..b504b4c00 100644
--- a/src/mailman/bin/onebounce.py
+++ b/src/mailman/bin/onebounce.py
@@ -18,9 +18,6 @@
"""Test bounce detection on message files."""
-from __future__ import absolute_import, print_function, unicode_literals
-
-__metaclass__ = type
__all__ = [
'main',
]
diff --git a/src/mailman/bin/runner.py b/src/mailman/bin/runner.py
index 7648ed961..88e02254f 100644
--- a/src/mailman/bin/runner.py
+++ b/src/mailman/bin/runner.py
@@ -17,9 +17,6 @@
"""The runner process."""
-from __future__ import absolute_import, print_function, unicode_literals
-
-__metaclass__ = type
__all__ = [
'main',
]
diff --git a/src/mailman/bin/tests/test_master.py b/src/mailman/bin/tests/test_master.py
index d6e301e58..c65777e5e 100644
--- a/src/mailman/bin/tests/test_master.py
+++ b/src/mailman/bin/tests/test_master.py
@@ -17,9 +17,6 @@
"""Test master watcher utilities."""
-from __future__ import absolute_import, print_function, unicode_literals
-
-__metaclass__ = type
__all__ = [
'TestMasterLock',
]
diff --git a/src/mailman/chains/accept.py b/src/mailman/chains/accept.py
index f5dd5a73d..89995b5a1 100644
--- a/src/mailman/chains/accept.py
+++ b/src/mailman/chains/accept.py
@@ -17,9 +17,6 @@
"""The terminal 'accept' chain."""
-from __future__ import absolute_import, print_function, unicode_literals
-
-__metaclass__ = type
__all__ = [
'AcceptChain',
]
@@ -27,12 +24,11 @@ __all__ = [
import logging
-from zope.event import notify
-
from mailman.chains.base import TerminalChainBase
from mailman.config import config
from mailman.core.i18n import _
from mailman.interfaces.chain import AcceptEvent
+from zope.event import notify
log = logging.getLogger('mailman.vette')
diff --git a/src/mailman/chains/base.py b/src/mailman/chains/base.py
index 37d8e76f3..7db31de73 100644
--- a/src/mailman/chains/base.py
+++ b/src/mailman/chains/base.py
@@ -17,9 +17,6 @@
"""Base class for terminal chains."""
-from __future__ import absolute_import, print_function, unicode_literals
-
-__metaclass__ = type
__all__ = [
'Chain',
'Link',
@@ -27,11 +24,10 @@ __all__ = [
]
-from zope.interface import implementer
-
from mailman.config import config
from mailman.interfaces.chain import (
IChain, IChainIterator, IChainLink, IMutableChain, LinkAction)
+from zope.interface import implementer
diff --git a/src/mailman/chains/builtin.py b/src/mailman/chains/builtin.py
index bce9349a1..b26b31550 100644
--- a/src/mailman/chains/builtin.py
+++ b/src/mailman/chains/builtin.py
@@ -17,9 +17,6 @@
"""The default built-in starting chain."""
-from __future__ import absolute_import, print_function, unicode_literals
-
-__metaclass__ = type
__all__ = [
'BuiltInChain',
]
@@ -27,12 +24,11 @@ __all__ = [
import logging
-from zope.interface import implementer
-
from mailman.chains.base import Link
from mailman.config import config
from mailman.core.i18n import _
from mailman.interfaces.chain import IChain, LinkAction
+from zope.interface import implementer
log = logging.getLogger('mailman.vette')
diff --git a/src/mailman/chains/discard.py b/src/mailman/chains/discard.py
index 001b243ac..9eb419201 100644
--- a/src/mailman/chains/discard.py
+++ b/src/mailman/chains/discard.py
@@ -17,20 +17,17 @@
"""The terminal 'discard' chain."""
-from __future__ import absolute_import, print_function, unicode_literals
-
-__metaclass__ = type
__all__ = [
'DiscardChain',
]
import logging
-from zope.event import notify
from mailman.chains.base import TerminalChainBase
from mailman.core.i18n import _
from mailman.interfaces.chain import DiscardEvent
+from zope.event import notify
log = logging.getLogger('mailman.vette')
diff --git a/src/mailman/chains/headers.py b/src/mailman/chains/headers.py
index 7628c8b7c..5738336e8 100644
--- a/src/mailman/chains/headers.py
+++ b/src/mailman/chains/headers.py
@@ -17,9 +17,6 @@
"""The header-matching chain."""
-from __future__ import absolute_import, print_function, unicode_literals
-
-__metaclass__ = type
__all__ = [
'HeaderMatchChain',
]
@@ -28,13 +25,12 @@ __all__ = [
import re
import logging
-from zope.interface import implementer
-
from mailman.chains.base import Chain, Link
from mailman.config import config
from mailman.core.i18n import _
from mailman.interfaces.chain import LinkAction
from mailman.interfaces.rules import IRule
+from zope.interface import implementer
log = logging.getLogger('mailman.error')
@@ -122,7 +118,7 @@ class HeaderMatchChain(Chain):
"""See `IMutableChain`."""
# Remove all dynamically created rules. Use the keys so we can mutate
# the dictionary inside the loop.
- for rule_name in config.rules.keys():
+ for rule_name in list(config.rules):
if rule_name.startswith('header-match-'):
del config.rules[rule_name]
self._extended_links = []
diff --git a/src/mailman/chains/hold.py b/src/mailman/chains/hold.py
index 1293ea266..7a516dc0d 100644
--- a/src/mailman/chains/hold.py
+++ b/src/mailman/chains/hold.py
@@ -17,9 +17,6 @@
"""The terminal 'hold' chain."""
-from __future__ import absolute_import, print_function, unicode_literals
-
-__metaclass__ = type
__all__ = [
'HoldChain',
]
@@ -30,10 +27,6 @@ import logging
from email.mime.message import MIMEMessage
from email.mime.text import MIMEText
from email.utils import formatdate, make_msgid
-from zope.component import getUtility
-from zope.event import notify
-from zope.interface import implementer
-
from mailman.app.moderator import hold_message
from mailman.app.replybot import can_acknowledge
from mailman.chains.base import TerminalChainBase
@@ -47,6 +40,9 @@ from mailman.interfaces.pending import IPendable, IPendings
from mailman.interfaces.usermanager import IUserManager
from mailman.utilities.i18n import make
from mailman.utilities.string import oneline, wrap
+from zope.component import getUtility
+from zope.event import notify
+from zope.interface import implementer
log = logging.getLogger('mailman.vette')
@@ -157,7 +153,7 @@ class HoldChain(TerminalChainBase):
if original_subject is None:
original_subject = _('(no subject)')
else:
- original_subject = oneline(original_subject, charset)
+ original_subject = oneline(original_subject, in_unicode=True)
substitutions = dict(
listname = mlist.fqdn_listname,
subject = original_subject,
diff --git a/src/mailman/chains/moderation.py b/src/mailman/chains/moderation.py
index 9b34f6389..944a66089 100644
--- a/src/mailman/chains/moderation.py
+++ b/src/mailman/chains/moderation.py
@@ -34,21 +34,17 @@ made as to the disposition of the message. `defer` is the default for
members, while `hold` is the default for nonmembers.
"""
-from __future__ import absolute_import, print_function, unicode_literals
-
-__metaclass__ = type
__all__ = [
'ModerationChain',
]
-from zope.interface import implementer
-
from mailman.chains.base import Link
from mailman.config import config
from mailman.core.i18n import _
from mailman.interfaces.action import Action
from mailman.interfaces.chain import IChain, LinkAction
+from zope.interface import implementer
diff --git a/src/mailman/chains/owner.py b/src/mailman/chains/owner.py
index 8e9aac154..9b0670ac9 100644
--- a/src/mailman/chains/owner.py
+++ b/src/mailman/chains/owner.py
@@ -17,9 +17,6 @@
"""The standard -owner posting chain."""
-from __future__ import absolute_import, print_function, unicode_literals
-
-__metaclass__ = type
__all__ = [
'BuiltInOwnerChain',
]
@@ -27,12 +24,11 @@ __all__ = [
import logging
-from zope.event import notify
-
from mailman.chains.base import TerminalChainBase
from mailman.config import config
from mailman.core.i18n import _
from mailman.interfaces.chain import AcceptOwnerEvent
+from zope.event import notify
log = logging.getLogger('mailman.vette')
diff --git a/src/mailman/chains/reject.py b/src/mailman/chains/reject.py
index e24cedb85..2f358afe1 100644
--- a/src/mailman/chains/reject.py
+++ b/src/mailman/chains/reject.py
@@ -17,9 +17,6 @@
"""The terminal 'reject' chain."""
-from __future__ import absolute_import, print_function, unicode_literals
-
-__metaclass__ = type
__all__ = [
'RejectChain',
]
@@ -27,12 +24,11 @@ __all__ = [
import logging
-from zope.event import notify
-
from mailman.app.bounces import bounce_message
from mailman.chains.base import TerminalChainBase
from mailman.core.i18n import _
from mailman.interfaces.chain import RejectEvent
+from zope.event import notify
log = logging.getLogger('mailman.vette')
diff --git a/src/mailman/chains/tests/test_base.py b/src/mailman/chains/tests/test_base.py
index 8d0d70449..784309395 100644
--- a/src/mailman/chains/tests/test_base.py
+++ b/src/mailman/chains/tests/test_base.py
@@ -17,9 +17,6 @@
"""Test the base chain stuff."""
-from __future__ import absolute_import, print_function, unicode_literals
-
-__metaclass__ = type
__all__ = [
'TestMiscellaneous',
]
diff --git a/src/mailman/chains/tests/test_headers.py b/src/mailman/chains/tests/test_headers.py
index adfc0ecb6..55bed3af0 100644
--- a/src/mailman/chains/tests/test_headers.py
+++ b/src/mailman/chains/tests/test_headers.py
@@ -17,9 +17,6 @@
"""Test the header chain."""
-from __future__ import absolute_import, print_function, unicode_literals
-
-__metaclass__ = type
__all__ = [
'TestHeaderChain',
]
diff --git a/src/mailman/chains/tests/test_hold.py b/src/mailman/chains/tests/test_hold.py
index a1fddd558..2a49b0ff0 100644
--- a/src/mailman/chains/tests/test_hold.py
+++ b/src/mailman/chains/tests/test_hold.py
@@ -17,9 +17,6 @@
"""Additional tests for the hold chain."""
-from __future__ import absolute_import, print_function, unicode_literals
-
-__metaclass__ = type
__all__ = [
'TestAutorespond',
]
@@ -27,14 +24,13 @@ __all__ = [
import unittest
-from zope.component import getUtility
-
from mailman.app.lifecycle import create_list
from mailman.chains.hold import autorespond_to_sender
from mailman.interfaces.autorespond import IAutoResponseSet, Response
from mailman.interfaces.usermanager import IUserManager
from mailman.testing.helpers import configuration, get_queue_messages
from mailman.testing.layers import ConfigLayer
+from zope.component import getUtility
diff --git a/src/mailman/chains/tests/test_owner.py b/src/mailman/chains/tests/test_owner.py
index 96b858317..0766ba630 100644
--- a/src/mailman/chains/tests/test_owner.py
+++ b/src/mailman/chains/tests/test_owner.py
@@ -17,9 +17,6 @@
"""Test the owner chain."""
-from __future__ import absolute_import, print_function, unicode_literals
-
-__metaclass__ = type
__all__ = [
'TestOwnerChain',
]
@@ -32,8 +29,7 @@ from mailman.chains.owner import BuiltInOwnerChain
from mailman.core.chains import process
from mailman.interfaces.chain import AcceptOwnerEvent
from mailman.testing.helpers import (
- event_subscribers,
- get_queue_messages,
+ event_subscribers, get_queue_messages,
specialized_message_from_string as mfs)
from mailman.testing.layers import ConfigLayer
diff --git a/src/mailman/commands/cli_aliases.py b/src/mailman/commands/cli_aliases.py
index 7c85ad9e0..2e1dc88ec 100644
--- a/src/mailman/commands/cli_aliases.py
+++ b/src/mailman/commands/cli_aliases.py
@@ -17,20 +17,16 @@
"""Generate Mailman alias files for your MTA."""
-from __future__ import absolute_import, print_function, unicode_literals
-
-__metaclass__ = type
__all__ = [
'Aliases',
]
-from zope.interface import implementer
-
from mailman.config import config
from mailman.core.i18n import _
from mailman.interfaces.command import ICLISubCommand
from mailman.utilities.modules import call_name
+from zope.interface import implementer
diff --git a/src/mailman/commands/cli_conf.py b/src/mailman/commands/cli_conf.py
index 7fe9fce7d..d0b7f7d2f 100644
--- a/src/mailman/commands/cli_conf.py
+++ b/src/mailman/commands/cli_conf.py
@@ -17,9 +17,6 @@
"""Print the mailman configuration."""
-from __future__ import absolute_import, print_function, unicode_literals
-
-__metaclass__ = type
__all__ = [
'Conf'
]
@@ -29,11 +26,10 @@ import sys
from contextlib import closing
from lazr.config._config import Section
-from zope.interface import implementer
-
from mailman.config import config
from mailman.core.i18n import _
from mailman.interfaces.command import ICLISubCommand
+from zope.interface import implementer
diff --git a/src/mailman/commands/cli_control.py b/src/mailman/commands/cli_control.py
index b0afc1337..de3542106 100644
--- a/src/mailman/commands/cli_control.py
+++ b/src/mailman/commands/cli_control.py
@@ -15,11 +15,8 @@
# You should have received a copy of the GNU General Public License along with
# GNU Mailman. If not, see <http://www.gnu.org/licenses/>.
-"""Module stuff."""
+"""Start/stop/reopen/restart commands."""
-from __future__ import absolute_import, print_function, unicode_literals
-
-__metaclass__ = type
__all__ = [
'Reopen',
'Restart',
@@ -34,12 +31,11 @@ import errno
import signal
import logging
-from zope.interface import implementer
-
from mailman.bin.master import WatcherState, master_state
from mailman.config import config
from mailman.core.i18n import _
from mailman.interfaces.command import ICLISubCommand
+from zope.interface import implementer
qlog = logging.getLogger('mailman.runner')
@@ -124,8 +120,8 @@ class Start:
# subprocesses to calculate their path to the $VAR_DIR. Before we
# chdir() though, calculate the absolute path to the configuration
# file.
- config_path = (os.path.abspath(args.config)
- if args.config else None)
+ config_path = (config.filename if args.config is None
+ else os.path.abspath(args.config))
os.environ['MAILMAN_VAR_DIR'] = config.VAR_DIR
os.chdir(config.VAR_DIR)
# Exec the master watcher.
@@ -135,8 +131,9 @@ class Start:
]
if args.force:
execl_args.append('--force')
- if config_path:
- execl_args.extend(['-C', config_path])
+ # Always pass the config file path to the master projects, so there's
+ # no confusion about which cfg is being used.
+ execl_args.extend(['-C', config_path])
qlog.debug('starting: %s', execl_args)
os.execl(*execl_args)
# We should never get here.
diff --git a/src/mailman/commands/cli_help.py b/src/mailman/commands/cli_help.py
index ce39eeda5..721c8936e 100644
--- a/src/mailman/commands/cli_help.py
+++ b/src/mailman/commands/cli_help.py
@@ -17,17 +17,13 @@
"""The 'help' subcommand."""
-from __future__ import absolute_import, print_function, unicode_literals
-
-__metaclass__ = type
__all__ = [
'Help',
]
-from zope.interface import implementer
-
from mailman.interfaces.command import ICLISubCommand
+from zope.interface import implementer
diff --git a/src/mailman/commands/cli_import.py b/src/mailman/commands/cli_import.py
index 5e25cd4fe..38b6fcef4 100644
--- a/src/mailman/commands/cli_import.py
+++ b/src/mailman/commands/cli_import.py
@@ -17,25 +17,21 @@
"""Importing list data into Mailman 3."""
-from __future__ import absolute_import, print_function, unicode_literals
-
-__metaclass__ = type
__all__ = [
'Import21',
]
import sys
-import cPickle
-
-from zope.component import getUtility
-from zope.interface import implementer
from mailman.core.i18n import _
from mailman.database.transaction import transactional
from mailman.interfaces.command import ICLISubCommand
from mailman.interfaces.listmanager import IListManager
from mailman.utilities.importer import import_config_pck, Import21Error
+from six.moves import cPickle
+from zope.component import getUtility
+from zope.interface import implementer
@@ -78,7 +74,7 @@ class Import21:
assert len(args.pickle_file) == 1, (
'Unexpected positional arguments: %s' % args.pickle_file)
filename = args.pickle_file[0]
- with open(filename) as fp:
+ with open(filename, 'rb') as fp:
while True:
try:
config_dict = cPickle.load(fp)
diff --git a/src/mailman/commands/cli_info.py b/src/mailman/commands/cli_info.py
index 4304e0ddb..6dd938127 100644
--- a/src/mailman/commands/cli_info.py
+++ b/src/mailman/commands/cli_info.py
@@ -17,9 +17,6 @@
"""Information about this Mailman instance."""
-from __future__ import absolute_import, print_function, unicode_literals
-
-__metaclass__ = type
__all__ = [
'Info'
]
@@ -28,13 +25,12 @@ __all__ = [
import sys
from lazr.config import as_boolean
-from zope.interface import implementer
-
from mailman.config import config
from mailman.core.i18n import _
from mailman.interfaces.command import ICLISubCommand
from mailman.rest.helpers import path_to
from mailman.version import MAILMAN_VERSION_FULL
+from zope.interface import implementer
diff --git a/src/mailman/commands/cli_inject.py b/src/mailman/commands/cli_inject.py
index 07ef0ec6c..ad4b53291 100644
--- a/src/mailman/commands/cli_inject.py
+++ b/src/mailman/commands/cli_inject.py
@@ -17,9 +17,6 @@
"""bin/mailman inject"""
-from __future__ import absolute_import, print_function, unicode_literals
-
-__metaclass__ = type
__all__ = [
'Inject',
]
@@ -27,14 +24,13 @@ __all__ = [
import sys
-from zope.component import getUtility
-from zope.interface import implementer
-
from mailman.app.inject import inject_text
from mailman.config import config
from mailman.core.i18n import _
from mailman.interfaces.command import ICLISubCommand
from mailman.interfaces.listmanager import IListManager
+from zope.component import getUtility
+from zope.interface import implementer
@@ -49,7 +45,7 @@ class Inject:
self.parser = parser
command_parser.add_argument(
'-q', '--queue',
- type=unicode, help=_("""
+ help=_("""
The name of the queue to inject the message to. QUEUE must be one
of the directories inside the qfiles directory. If omitted, the
incoming queue is used."""))
@@ -59,7 +55,7 @@ class Inject:
help=_('Show a list of all available queue names and exit.'))
command_parser.add_argument(
'-f', '--filename',
- type=unicode, help=_("""
+ help=_("""
Name of file containing the message to inject. If not given, or
'-' (without the quotes) standard input is used."""))
# Required positional argument.
diff --git a/src/mailman/commands/cli_lists.py b/src/mailman/commands/cli_lists.py
index cf1bd2ead..fac1dcd1d 100644
--- a/src/mailman/commands/cli_lists.py
+++ b/src/mailman/commands/cli_lists.py
@@ -17,9 +17,6 @@
"""The 'lists' subcommand."""
-from __future__ import absolute_import, print_function, unicode_literals
-
-__metaclass__ = type
__all__ = [
'Create',
'Lists',
@@ -27,9 +24,6 @@ __all__ = [
]
-from zope.component import getUtility
-from zope.interface import implementer
-
from mailman.app.lifecycle import create_list, remove_list
from mailman.core.constants import system_preferences
from mailman.core.i18n import _
@@ -43,6 +37,8 @@ from mailman.interfaces.domain import (
from mailman.interfaces.languages import ILanguageManager
from mailman.interfaces.listmanager import IListManager, ListAlreadyExistsError
from mailman.utilities.i18n import make
+from zope.component import getUtility
+from zope.interface import implementer
COMMASPACE = ', '
@@ -135,12 +131,12 @@ class Create:
self.parser = parser
command_parser.add_argument(
'--language',
- type=unicode, metavar='CODE', help=_("""\
+ metavar='CODE', help=_("""\
Set the list's preferred language to CODE, which must be a
registered two letter language code."""))
command_parser.add_argument(
'-o', '--owner',
- type=unicode, action='append', default=[],
+ action='append', default=[],
dest='owners', metavar='OWNER', help=_("""\
Specify a listowner email address. If the address is not
currently registered with Mailman, the address is registered and
diff --git a/src/mailman/commands/cli_members.py b/src/mailman/commands/cli_members.py
index 291fda3b7..21d78ec54 100644
--- a/src/mailman/commands/cli_members.py
+++ b/src/mailman/commands/cli_members.py
@@ -17,9 +17,6 @@
"""The 'members' subcommand."""
-from __future__ import absolute_import, print_function, unicode_literals
-
-__metaclass__ = type
__all__ = [
'Members',
]
@@ -29,11 +26,6 @@ import sys
import codecs
from email.utils import formataddr, parseaddr
-from operator import attrgetter
-from passlib.utils import generate_password as generate
-from zope.component import getUtility
-from zope.interface import implementer
-
from mailman.app.membership import add_member
from mailman.config import config
from mailman.core.i18n import _
@@ -42,6 +34,10 @@ from mailman.interfaces.command import ICLISubCommand
from mailman.interfaces.listmanager import IListManager
from mailman.interfaces.member import (
AlreadySubscribedError, DeliveryMode, DeliveryStatus)
+from operator import attrgetter
+from passlib.utils import generate_password as generate
+from zope.component import getUtility
+from zope.interface import implementer
@@ -197,8 +193,6 @@ class Members:
continue
# Parse the line and ensure that the values are unicodes.
display_name, email = parseaddr(line)
- display_name = display_name.decode(fp.encoding)
- email = email.decode(fp.encoding)
# Give the user a default, user-friendly password.
password = generate(int(config.passwords.password_length))
try:
diff --git a/src/mailman/commands/cli_qfile.py b/src/mailman/commands/cli_qfile.py
index 986898bee..e502deac8 100644
--- a/src/mailman/commands/cli_qfile.py
+++ b/src/mailman/commands/cli_qfile.py
@@ -17,24 +17,22 @@
"""Getting information out of a qfile."""
-from __future__ import absolute_import, print_function, unicode_literals
-
-__metaclass__ = type
__all__ = [
'QFile',
]
-import cPickle
-
-from pprint import PrettyPrinter
-from zope.interface import implementer
+import six
from mailman.core.i18n import _
from mailman.interfaces.command import ICLISubCommand
from mailman.utilities.interact import interact
+from pprint import PrettyPrinter
+from six.moves import cPickle
+from zope.interface import implementer
+# This is deliberately called 'm' for use with --interactive.
m = []
@@ -71,7 +69,7 @@ class QFile:
"""See `ICLISubCommand`."""
printer = PrettyPrinter(indent=4)
assert len(args.qfile) == 1, 'Wrong number of positional arguments'
- with open(args.qfile[0]) as fp:
+ with open(args.qfile[0], 'rb') as fp:
while True:
try:
m.append(cPickle.load(fp))
@@ -82,7 +80,7 @@ class QFile:
for i, obj in enumerate(m):
count = i + 1
print(_('<----- start object $count ----->'))
- if isinstance(obj, basestring):
+ if isinstance(obj, six.string_types):
print(obj)
else:
printer.pprint(obj)
diff --git a/src/mailman/commands/cli_status.py b/src/mailman/commands/cli_status.py
index 207b44e04..2bef9d73c 100644
--- a/src/mailman/commands/cli_status.py
+++ b/src/mailman/commands/cli_status.py
@@ -17,9 +17,6 @@
"""bin/mailman status."""
-from __future__ import absolute_import, print_function, unicode_literals
-
-__metaclass__ = type
__all__ = [
'Status',
]
@@ -27,11 +24,10 @@ __all__ = [
import socket
-from zope.interface import implementer
-
from mailman.bin.master import WatcherState, master_state
from mailman.core.i18n import _
from mailman.interfaces.command import ICLISubCommand
+from zope.interface import implementer
diff --git a/src/mailman/commands/cli_unshunt.py b/src/mailman/commands/cli_unshunt.py
index 77196565b..7cfa9e4ed 100644
--- a/src/mailman/commands/cli_unshunt.py
+++ b/src/mailman/commands/cli_unshunt.py
@@ -17,9 +17,6 @@
"""The 'unshunt' command."""
-from __future__ import absolute_import, print_function, unicode_literals
-
-__metaclass__ = type
__all__ = [
'Unshunt',
]
@@ -27,11 +24,10 @@ __all__ = [
import sys
-from zope.interface import implementer
-
from mailman.config import config
from mailman.core.i18n import _
from mailman.interfaces.command import ICLISubCommand
+from zope.interface import implementer
@@ -62,7 +58,7 @@ class Unshunt:
which_queue = msgdata.get('whichq', 'in')
if not args.discard:
config.switchboards[which_queue].enqueue(msg, msgdata)
- except Exception as error:
+ except Exception:
print(_('Cannot unshunt message $filebase, skipping:\n$error'),
file=sys.stderr)
else:
diff --git a/src/mailman/commands/cli_version.py b/src/mailman/commands/cli_version.py
index 86ce9ab68..bc0f34a34 100644
--- a/src/mailman/commands/cli_version.py
+++ b/src/mailman/commands/cli_version.py
@@ -17,18 +17,14 @@
"""The Mailman version."""
-from __future__ import absolute_import, print_function, unicode_literals
-
-__metaclass__ = type
__all__ = [
'Version',
]
-from zope.interface import implementer
-
from mailman.interfaces.command import ICLISubCommand
from mailman.version import MAILMAN_VERSION_FULL
+from zope.interface import implementer
diff --git a/src/mailman/commands/cli_withlist.py b/src/mailman/commands/cli_withlist.py
index fc2363816..7cf8c0451 100644
--- a/src/mailman/commands/cli_withlist.py
+++ b/src/mailman/commands/cli_withlist.py
@@ -17,9 +17,6 @@
"""bin/mailman withlist"""
-from __future__ import absolute_import, print_function, unicode_literals
-
-__metaclass__ = type
__all__ = [
'Shell',
'Withlist',
@@ -30,15 +27,15 @@ import re
import sys
from lazr.config import as_boolean
-from zope.component import getUtility
-from zope.interface import implementer
-
from mailman.config import config
from mailman.core.i18n import _
from mailman.interfaces.command import ICLISubCommand
from mailman.interfaces.listmanager import IListManager
from mailman.utilities.interact import DEFAULT_BANNER, interact
from mailman.utilities.modules import call_name
+from zope.component import getUtility
+from zope.interface import implementer
+
# Global holding onto the open mailing list.
m = None
diff --git a/src/mailman/commands/docs/echo.rst b/src/mailman/commands/docs/echo.rst
index 32399ebfc..6412a4afe 100644
--- a/src/mailman/commands/docs/echo.rst
+++ b/src/mailman/commands/docs/echo.rst
@@ -24,7 +24,7 @@ The original message is ignored, but the results receive the echoed command.
>>> from mailman.email.message import Message
>>> print(command.process(mlist, Message(), {}, ('foo', 'bar'), results))
ContinueProcessing.yes
- >>> print(unicode(results))
+ >>> print(str(results))
The results of your email command are provided below.
<BLANKLINE>
echo foo bar
diff --git a/src/mailman/commands/docs/help.rst b/src/mailman/commands/docs/help.rst
index bbd6c8c09..5330a0b79 100644
--- a/src/mailman/commands/docs/help.rst
+++ b/src/mailman/commands/docs/help.rst
@@ -25,7 +25,7 @@ short description of each of them.
>>> from mailman.email.message import Message
>>> print(help.process(mlist, Message(), {}, (), results))
ContinueProcessing.yes
- >>> print(unicode(results))
+ >>> print(results)
The results of your email command are provided below.
<BLANKLINE>
confirm - Confirm a subscription request.
@@ -44,19 +44,19 @@ With an argument, you can get more detailed help about a specific command.
>>> results = Results()
>>> print(help.process(mlist, Message(), {}, ('help',), results))
ContinueProcessing.yes
- >>> print(unicode(results))
+ >>> print(results)
The results of your email command are provided below.
<BLANKLINE>
help [command]
Get help about available email commands.
<BLANKLINE>
-
+
Some commands have even more detailed help.
>>> results = Results()
>>> print(help.process(mlist, Message(), {}, ('join',), results))
ContinueProcessing.yes
- >>> print(unicode(results))
+ >>> print(results)
The results of your email command are provided below.
<BLANKLINE>
join [digest=<no|mime|plain>]
diff --git a/src/mailman/commands/docs/info.rst b/src/mailman/commands/docs/info.rst
index 8bc7579e6..6ce223403 100644
--- a/src/mailman/commands/docs/info.rst
+++ b/src/mailman/commands/docs/info.rst
@@ -62,20 +62,21 @@ definition.
Python ...
...
File system paths:
- ARCHIVE_DIR = /var/lib/mailman/archives
- BIN_DIR = /sbin
- DATA_DIR = /var/lib/mailman/data
- ETC_DIR = /etc
- EXT_DIR = /etc/mailman.d
- LIST_DATA_DIR = /var/lib/mailman/lists
- LOCK_DIR = /var/lock/mailman
- LOCK_FILE = /var/lock/mailman/master.lck
- LOG_DIR = /var/log/mailman
- MESSAGES_DIR = /var/lib/mailman/messages
- PID_FILE = /var/run/mailman/master.pid
- QUEUE_DIR = /var/spool/mailman
- TEMPLATE_DIR = .../mailman/templates
- VAR_DIR = /var/lib/mailman
+ ARCHIVE_DIR = /var/lib/mailman/archives
+ BIN_DIR = /sbin
+ CFG_FILE = .../test.cfg
+ DATA_DIR = /var/lib/mailman/data
+ ETC_DIR = /etc
+ EXT_DIR = /etc/mailman.d
+ LIST_DATA_DIR = /var/lib/mailman/lists
+ LOCK_DIR = /var/lock/mailman
+ LOCK_FILE = /var/lock/mailman/master.lck
+ LOG_DIR = /var/log/mailman
+ MESSAGES_DIR = /var/lib/mailman/messages
+ PID_FILE = /var/run/mailman/master.pid
+ QUEUE_DIR = /var/spool/mailman
+ TEMPLATE_DIR = .../mailman/templates
+ VAR_DIR = /var/lib/mailman
.. _`Filesystem Hierarchy Standard`: http://www.pathname.com/fhs/
diff --git a/src/mailman/commands/docs/inject.rst b/src/mailman/commands/docs/inject.rst
index 63e7b0366..de295b8f6 100644
--- a/src/mailman/commands/docs/inject.rst
+++ b/src/mailman/commands/docs/inject.rst
@@ -94,7 +94,7 @@ By default, the incoming queue is used.
>>> dump_msgdata(items[0].msgdata)
_parsemsg : False
- listname : test@example.com
+ listid : test.example.com
original_size: 203
version : 3
@@ -122,7 +122,7 @@ But a different queue can be specified on the command line.
>>> dump_msgdata(items[0].msgdata)
_parsemsg : False
- listname : test@example.com
+ listid : test.example.com
original_size: 203
version : 3
@@ -133,7 +133,7 @@ Standard input
The message text can also be provided on standard input.
::
- >>> from StringIO import StringIO
+ >>> from six import StringIO
# Remember: we've got unicode literals turned on.
>>> standard_in = StringIO(str("""\
@@ -167,7 +167,7 @@ The message text can also be provided on standard input.
>>> dump_msgdata(items[0].msgdata)
_parsemsg : False
- listname : test@example.com
+ listid : test.example.com
original_size: 211
version : 3
@@ -195,7 +195,7 @@ injected.
_parsemsg : False
bar : two
foo : one
- listname : test@example.com
+ listid : test.example.com
original_size: 203
version : 3
diff --git a/src/mailman/commands/docs/members.rst b/src/mailman/commands/docs/members.rst
index 7b99e92f9..28f238f31 100644
--- a/src/mailman/commands/docs/members.rst
+++ b/src/mailman/commands/docs/members.rst
@@ -229,15 +229,14 @@ You can also specify ``-`` as the filename, in which case the addresses are
taken from standard input.
::
- >>> from StringIO import StringIO
+ >>> from six import StringIO
>>> fp = StringIO()
- >>> fp.encoding = 'us-ascii'
>>> for address in ('dperson@example.com',
... 'Elly Person <eperson@example.com>',
... 'fperson@example.com (Fred Person)',
... ):
... print(address, file=fp)
- >>> fp.seek(0)
+ >>> filepos = fp.seek(0)
>>> import sys
>>> sys.stdin = fp
diff --git a/src/mailman/commands/docs/membership.rst b/src/mailman/commands/docs/membership.rst
index aa3ab97e6..a260e930a 100644
--- a/src/mailman/commands/docs/membership.rst
+++ b/src/mailman/commands/docs/membership.rst
@@ -45,7 +45,7 @@ If that's missing though, then an error is returned.
>>> from mailman.email.message import Message
>>> print(join.process(mlist, Message(), {}, (), results))
ContinueProcessing.no
- >>> print(unicode(results))
+ >>> print(results)
The results of your email command are provided below.
<BLANKLINE>
join: No valid address found to subscribe
@@ -60,7 +60,7 @@ The ``subscribe`` command is an alias.
>>> results = Results()
>>> print(subscribe.process(mlist, Message(), {}, (), results))
ContinueProcessing.no
- >>> print(unicode(results))
+ >>> print(results)
The results of your email command are provided below.
<BLANKLINE>
subscribe: No valid address found to subscribe
@@ -79,7 +79,7 @@ When the message has a From field, that address will be subscribed.
>>> results = Results()
>>> print(join.process(mlist, msg, {}, (), results))
ContinueProcessing.yes
- >>> print(unicode(results))
+ >>> print(results)
The results of your email command are provided below.
<BLANKLINE>
Confirmation email sent to Anne Person <anne@example.com>
@@ -150,7 +150,7 @@ list.
>>> results = Results()
>>> print(confirm.process(mlist, msg, {}, (token,), results))
ContinueProcessing.yes
- >>> print(unicode(results))
+ >>> print(results)
The results of your email command are provided below.
<BLANKLINE>
Confirmed
@@ -208,7 +208,7 @@ list.
>>> results = Results()
>>> print(confirm.process(mlist_2, msg, {}, (token,), results))
ContinueProcessing.yes
- >>> print(unicode(results))
+ >>> print(results)
The results of your email command are provided below.
<BLANKLINE>
Confirmed
@@ -241,7 +241,7 @@ is sent a confirmation message for her request.
>>> results = Results()
>>> print(leave.process(mlist_2, msg, {}, (), results))
ContinueProcessing.yes
- >>> print(unicode(results))
+ >>> print(results)
The results of your email command are provided below.
<BLANKLINE>
Anne Person <anne@example.com> left baker@example.com
@@ -278,7 +278,7 @@ to unsubscribe Anne from the alpha mailing list.
>>> print(leave.process(mlist, msg, {}, (), results))
ContinueProcessing.no
- >>> print(unicode(results))
+ >>> print(results)
The results of your email command are provided below.
<BLANKLINE>
Invalid or unverified email address: anne.person@example.org
@@ -299,7 +299,7 @@ unsubscribe her from the list.
>>> print(leave.process(mlist, msg, {}, (), results))
ContinueProcessing.yes
- >>> print(unicode(results))
+ >>> print(results)
The results of your email command are provided below.
<BLANKLINE>
Anne Person <anne.person@example.org> left alpha@example.com
@@ -354,7 +354,7 @@ a user of the system.
>>> print(confirm.process(mlist, msg, {}, (token,), results))
ContinueProcessing.yes
- >>> print(unicode(results))
+ >>> print(results)
The results of your email command are provided below.
<BLANKLINE>
Confirmed
diff --git a/src/mailman/commands/docs/qfile.rst b/src/mailman/commands/docs/qfile.rst
index 8ec0a3952..e097ebf97 100644
--- a/src/mailman/commands/docs/qfile.rst
+++ b/src/mailman/commands/docs/qfile.rst
@@ -47,7 +47,6 @@ Once we've figured out the file name of the shunted message, we can print it.
>>> command.process(FakeArgs)
[----- start pickle -----]
<----- start object 1 ----->
- From nobody ...
From: aperson@example.com
To: test@example.com
Subject: Uh oh
@@ -55,11 +54,7 @@ Once we've figured out the file name of the shunted message, we can print it.
I borkeded Mailman.
<BLANKLINE>
<----- start object 2 ----->
- { u'_parsemsg': False,
- 'bad': u'yes',
- 'bar': u'baz',
- 'foo': 7,
- u'version': 3}
+ {'_parsemsg': False, 'bad': 'yes', 'bar': 'baz', 'foo': 7, 'version': 3}
[----- end pickle -----]
Maybe we don't want to print the contents of the file though, in case we want
diff --git a/src/mailman/commands/docs/withlist.rst b/src/mailman/commands/docs/withlist.rst
index e915eb04c..321b6e68a 100644
--- a/src/mailman/commands/docs/withlist.rst
+++ b/src/mailman/commands/docs/withlist.rst
@@ -52,10 +52,10 @@ single argument, the mailing list.
>>> with open(os.path.join(config.VAR_DIR, 'showme.py'), 'w') as fp:
... print("""\
... def showme(mailing_list):
- ... print "The list's name is", mailing_list.fqdn_listname
+ ... print("The list's name is", mailing_list.fqdn_listname)
...
... def displayname(mailing_list):
- ... print "The list's display name is", mailing_list.display_name
+ ... print("The list's display name is", mailing_list.display_name)
... """, file=fp)
If the name of the function is the same as the module, then you only need to
diff --git a/src/mailman/commands/eml_confirm.py b/src/mailman/commands/eml_confirm.py
index 0239e0f25..2cef7cbad 100644
--- a/src/mailman/commands/eml_confirm.py
+++ b/src/mailman/commands/eml_confirm.py
@@ -15,22 +15,18 @@
# You should have received a copy of the GNU General Public License along with
# GNU Mailman. If not, see <http://www.gnu.org/licenses/>.
-"""Module stuff."""
+"""The 'confirm' email command."""
-from __future__ import absolute_import, print_function, unicode_literals
-
-__metaclass__ = type
__all__ = [
'Confirm',
]
-from zope.component import getUtility
-from zope.interface import implementer
-
from mailman.core.i18n import _
from mailman.interfaces.command import ContinueProcessing, IEmailCommand
from mailman.interfaces.registrar import IRegistrar
+from zope.component import getUtility
+from zope.interface import implementer
diff --git a/src/mailman/commands/eml_echo.py b/src/mailman/commands/eml_echo.py
index eb476dc7d..2bd55edbc 100644
--- a/src/mailman/commands/eml_echo.py
+++ b/src/mailman/commands/eml_echo.py
@@ -17,18 +17,14 @@
"""The email command 'echo'."""
-from __future__ import absolute_import, print_function, unicode_literals
-
-__metaclass__ = type
__all__ = [
'Echo',
]
-from zope.interface import implementer
-
from mailman.core.i18n import _
from mailman.interfaces.command import ContinueProcessing, IEmailCommand
+from zope.interface import implementer
SPACE = ' '
diff --git a/src/mailman/commands/eml_end.py b/src/mailman/commands/eml_end.py
index 447d4066b..d25c19fcb 100644
--- a/src/mailman/commands/eml_end.py
+++ b/src/mailman/commands/eml_end.py
@@ -17,19 +17,15 @@
"""The email commands 'end' and 'stop'."""
-from __future__ import absolute_import, print_function, unicode_literals
-
-__metaclass__ = type
__all__ = [
'End',
'Stop',
]
-from zope.interface import implementer
-
from mailman.core.i18n import _
from mailman.interfaces.command import ContinueProcessing, IEmailCommand
+from zope.interface import implementer
diff --git a/src/mailman/commands/eml_help.py b/src/mailman/commands/eml_help.py
index 139d484fb..8b93b272a 100644
--- a/src/mailman/commands/eml_help.py
+++ b/src/mailman/commands/eml_help.py
@@ -17,20 +17,16 @@
"""The email command 'help'."""
-from __future__ import absolute_import, print_function, unicode_literals
-
-__metaclass__ = type
__all__ = [
'Help',
]
-from zope.interface import implementer
-
from mailman.config import config
from mailman.core.i18n import _
from mailman.interfaces.command import ContinueProcessing, IEmailCommand
from mailman.utilities.string import wrap
+from zope.interface import implementer
SPACE = ' '
diff --git a/src/mailman/commands/eml_membership.py b/src/mailman/commands/eml_membership.py
index c56b14041..e6a6825ed 100644
--- a/src/mailman/commands/eml_membership.py
+++ b/src/mailman/commands/eml_membership.py
@@ -17,9 +17,6 @@
"""The email commands 'join' and 'subscribe'."""
-from __future__ import absolute_import, print_function, unicode_literals
-
-__metaclass__ = type
__all__ = [
'Join',
'Subscribe',
@@ -29,15 +26,14 @@ __all__ = [
from email.utils import formataddr, parseaddr
-from zope.component import getUtility
-from zope.interface import implementer
-
from mailman.core.i18n import _
from mailman.interfaces.command import ContinueProcessing, IEmailCommand
from mailman.interfaces.member import DeliveryMode, MemberRole
from mailman.interfaces.registrar import IRegistrar
from mailman.interfaces.subscriptions import ISubscriptionService
from mailman.interfaces.usermanager import IUserManager
+from zope.component import getUtility
+from zope.interface import implementer
@@ -182,6 +178,7 @@ You may be asked to confirm your request.""")
return ContinueProcessing.yes
+
class Unsubscribe(Leave):
"""The email 'unsubscribe' command (an alias for 'leave')."""
diff --git a/src/mailman/commands/tests/test_conf.py b/src/mailman/commands/tests/test_conf.py
index 12ed5c537..07036df3a 100644
--- a/src/mailman/commands/tests/test_conf.py
+++ b/src/mailman/commands/tests/test_conf.py
@@ -17,9 +17,6 @@
"""Test the conf subcommand."""
-from __future__ import absolute_import, print_function, unicode_literals
-
-__metaclass__ = type
__all__ = [
'TestConf',
]
@@ -31,9 +28,9 @@ import mock
import tempfile
import unittest
-from StringIO import StringIO
from mailman.commands.cli_conf import Conf
from mailman.testing.layers import ConfigLayer
+from six import StringIO
diff --git a/src/mailman/commands/tests/test_confirm.py b/src/mailman/commands/tests/test_confirm.py
index 19a9068bc..f067a2a0a 100644
--- a/src/mailman/commands/tests/test_confirm.py
+++ b/src/mailman/commands/tests/test_confirm.py
@@ -17,9 +17,6 @@
"""Test the `confirm` command."""
-from __future__ import absolute_import, print_function, unicode_literals
-
-__metaclass__ = type
__all__ = [
'TestConfirm',
]
@@ -27,8 +24,6 @@ __all__ = [
import unittest
-from zope.component import getUtility
-
from mailman.app.lifecycle import create_list
from mailman.commands.eml_confirm import Confirm
from mailman.email.message import Message
@@ -37,6 +32,7 @@ from mailman.interfaces.registrar import IRegistrar
from mailman.runners.command import Results
from mailman.testing.helpers import get_queue_messages, reset_the_world
from mailman.testing.layers import ConfigLayer
+from zope.component import getUtility
diff --git a/src/mailman/commands/tests/test_control.py b/src/mailman/commands/tests/test_control.py
index 0847d86b1..299f0da25 100644
--- a/src/mailman/commands/tests/test_control.py
+++ b/src/mailman/commands/tests/test_control.py
@@ -17,9 +17,6 @@
"""Test some additional corner cases for starting/stopping."""
-from __future__ import absolute_import, print_function, unicode_literals
-
-__metaclass__ = type
__all__ = [
'TestStart',
'find_master',
@@ -37,11 +34,11 @@ import socket
import unittest
from datetime import timedelta, datetime
-
from mailman.commands.cli_control import Start, kill_watcher
from mailman.config import config
from mailman.testing.layers import ConfigLayer
+
SEP = '|'
diff --git a/src/mailman/commands/tests/test_create.py b/src/mailman/commands/tests/test_create.py
index c2dffb929..47808c997 100644
--- a/src/mailman/commands/tests/test_create.py
+++ b/src/mailman/commands/tests/test_create.py
@@ -17,9 +17,6 @@
"""Test `bin/mailman create`."""
-from __future__ import absolute_import, print_function, unicode_literals
-
-__metaclass__ = type
__all__ = [
'TestCreate',
]
diff --git a/src/mailman/commands/tests/test_help.py b/src/mailman/commands/tests/test_help.py
index 3c7d1ae9f..b2de0297d 100644
--- a/src/mailman/commands/tests/test_help.py
+++ b/src/mailman/commands/tests/test_help.py
@@ -17,10 +17,8 @@
"""Additional tests for the `help` email command."""
-from __future__ import absolute_import, print_function, unicode_literals
-
-__metaclass__ = type
__all__ = [
+ 'TestHelp',
]
@@ -47,11 +45,11 @@ class TestHelp(unittest.TestCase):
def test_too_many_arguments(self):
# Error message when too many help arguments are given.
results = Results()
- status = self._help.process(self._mlist, Message(), {},
+ status = self._help.process(self._mlist, Message(), {},
('more', 'than', 'one'),
results)
self.assertEqual(status, ContinueProcessing.no)
- self.assertEqual(unicode(results), """\
+ self.assertEqual(str(results), """\
The results of your email command are provided below.
help: too many arguments: more than one
@@ -60,10 +58,10 @@ help: too many arguments: more than one
def test_no_such_command(self):
# Error message when asking for help on an existent command.
results = Results()
- status = self._help.process(self._mlist, Message(), {},
+ status = self._help.process(self._mlist, Message(), {},
('doesnotexist',), results)
self.assertEqual(status, ContinueProcessing.no)
- self.assertEqual(unicode(results), """\
+ self.assertEqual(str(results), """\
The results of your email command are provided below.
help: no such command: doesnotexist
diff --git a/src/mailman/config/__init__.py b/src/mailman/config/__init__.py
index fb240ad76..4b9b1d07a 100644
--- a/src/mailman/config/__init__.py
+++ b/src/mailman/config/__init__.py
@@ -17,9 +17,6 @@
"""Mailman configuration package."""
-from __future__ import absolute_import, print_function, unicode_literals
-
-__metaclass__ = type
__all__ = [
'config',
]
diff --git a/src/mailman/config/config.py b/src/mailman/config/config.py
index 7181e23e9..779fa27e5 100644
--- a/src/mailman/config/config.py
+++ b/src/mailman/config/config.py
@@ -17,9 +17,6 @@
"""Configuration file loading and management."""
-from __future__ import absolute_import, print_function, unicode_literals
-
-__metaclass__ = type
__all__ = [
'Configuration',
'external_configuration',
@@ -29,27 +26,28 @@ __all__ = [
import os
import sys
+import mailman.templates
-from ConfigParser import SafeConfigParser
from flufl.lock import Lock
from lazr.config import ConfigSchema, as_boolean
-from pkg_resources import resource_stream, resource_string
-from string import Template
-from zope.component import getUtility
-from zope.event import notify
-from zope.interface import implementer
-
-import mailman.templates
-
from mailman import version
from mailman.interfaces.configuration import (
ConfigurationUpdatedEvent, IConfiguration, MissingConfigurationFileError)
from mailman.interfaces.languages import ILanguageManager
from mailman.utilities.filesystem import makedirs
from mailman.utilities.modules import call_name, expand_path
+from pkg_resources import resource_filename, resource_string as resource_bytes
+from six.moves.configparser import ConfigParser, RawConfigParser
+from string import Template
+from unittest.mock import patch
+from zope.component import getUtility
+from zope.event import notify
+from zope.interface import implementer
SPACE = ' '
+SPACERS = '\n'
+
MAILMAN_CFG_TEMPLATE = """\
# AUTOMATICALLY GENERATED BY MAILMAN ON {}
@@ -66,6 +64,11 @@ MAILMAN_CFG_TEMPLATE = """\
# enabled: yes
# recipient: your.address@your.domain"""
+class _NonStrictRawConfigParser(RawConfigParser):
+ def __init__(self, *args, **kws):
+ kws['strict'] = False
+ super().__init__(*args, **kws)
+
@implementer(IConfiguration)
@@ -102,30 +105,29 @@ class Configuration:
def load(self, filename=None):
"""Load the configuration from the schema and config files."""
- schema_file = config_file = None
- try:
- schema_file = resource_stream('mailman.config', 'schema.cfg')
- schema = ConfigSchema('schema.cfg', schema_file)
- # 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_file = resource_stream('mailman.config', 'mailman.cfg')
- self._config = schema.loadFile(config_file, 'mailman.cfg')
- if filename is not None:
- self.filename = filename
- with open(filename) as user_config:
- self._config.push(filename, user_config.read())
- finally:
- if schema_file:
- schema_file.close()
- if config_file:
- config_file.close()
- self._post_process()
+ schema_file = resource_filename('mailman.config', 'schema.cfg')
+ schema = ConfigSchema(schema_file)
+ # 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_file = resource_filename('mailman.config', 'mailman.cfg')
+ self._config = schema.load(config_file)
+ if filename is None:
+ self._post_process()
+ else:
+ self.filename = filename
+ with open(filename, 'r', encoding='utf-8') as user_config:
+ self.push(filename, user_config.read())
def push(self, config_name, config_string):
"""Push a new configuration onto the stack."""
self._clear()
- self._config.push(config_name, config_string)
+ # In Python 3, the RawConfigParser() must be created with
+ # strict=False, otherwise we'll get a DuplicateSectionError.
+ # See https://bugs.launchpad.net/lazr.config/+bug/1397779
+ with patch('lazr.config._config.RawConfigParser',
+ _NonStrictRawConfigParser):
+ self._config.push(config_name, config_string)
self._post_process()
def pop(self, config_name):
@@ -164,6 +166,7 @@ class Configuration:
# path is relative.
var_dir = os.environ.get('MAILMAN_VAR_DIR', category.var_dir)
substitutions = dict(
+ cwd = os.getcwd(),
argv = bin_dir,
# Directories.
bin_dir = category.bin_dir,
@@ -185,26 +188,32 @@ class Configuration:
lock_file = category.lock_file,
pid_file = category.pid_file,
)
+ # Add the path to the .cfg file, if one was given on the command line.
+ if self.filename is not None:
+ substitutions['cfg_file'] = self.filename
# Now, perform substitutions recursively until there are no more
# variables with $-vars in them, or until substitutions are not
# helping any more.
last_dollar_count = 0
while True:
+ expandables = []
# Mutate the dictionary during iteration.
- dollar_count = 0
- for key in substitutions.keys():
+ for key in substitutions:
raw_value = substitutions[key]
value = Template(raw_value).safe_substitute(substitutions)
if '$' in value:
# Still more work to do.
- dollar_count += 1
+ expandables.append((key, value))
substitutions[key] = value
- if dollar_count == 0:
+ if len(expandables) == 0:
break
- if dollar_count == last_dollar_count:
- print('Path expansion infloop detected', file=sys.stderr)
+ if len(expandables) == last_dollar_count:
+ print('Path expansion infloop detected:\n',
+ SPACERS.join('\t{}: {}'.format(key, value)
+ for key, value in sorted(expandables)),
+ file=sys.stderr)
sys.exit(1)
- last_dollar_count = dollar_count
+ last_dollar_count = len(expandables)
# Ensure that all paths are normalized and made absolute. Handle the
# few special cases first. Most of these are due to backward
# compatibility.
@@ -269,7 +278,7 @@ class Configuration:
-def load_external(path, encoding=None):
+def load_external(path):
"""Load the configuration file named by path.
:param path: A string naming the location of the external configuration
@@ -278,21 +287,16 @@ def load_external(path, encoding=None):
value must name a ``.cfg`` file located within Python's import path,
however the trailing ``.cfg`` suffix is implied (don't provide it
here).
- :param encoding: The encoding to apply to the data read from path. If
- None, then bytes will be returned.
- :return: A unicode string or bytes, depending on ``encoding``.
+ :return: The contents of the configuration file.
+ :rtype: str
"""
# Is the context coming from a file system or Python path?
if path.startswith('python:'):
resource_path = path[7:]
package, dot, resource = resource_path.rpartition('.')
- config_string = resource_string(package, resource + '.cfg')
- else:
- with open(path, 'rb') as fp:
- config_string = fp.read()
- if encoding is None:
- return config_string
- return config_string.decode(encoding)
+ return resource_bytes(package, resource + '.cfg').decode('utf-8')
+ with open(path, 'r', encoding='utf-8') as fp:
+ return fp.read()
def external_configuration(path):
@@ -308,7 +312,7 @@ def external_configuration(path):
"""
# Is the context coming from a file system or Python path?
cfg_path = expand_path(path)
- parser = SafeConfigParser()
+ parser = ConfigParser()
files = parser.read(cfg_path)
if files != [cfg_path]:
raise MissingConfigurationFileError(path)
diff --git a/src/mailman/config/mailman.cfg b/src/mailman/config/mailman.cfg
index 24e81ec91..aea420280 100644
--- a/src/mailman/config/mailman.cfg
+++ b/src/mailman/config/mailman.cfg
@@ -23,9 +23,13 @@
# /var/tmp/mailman
[paths.dev]
-# Convenient development layout where everything is put in the current
-# directory.
-var_dir: var
+# Convenient development layout where everything is put in a directory above
+# where the mailman.cfg file lives.
+var_dir: $cfg_file/../..
+
+[paths.here]
+# Layout where the var directory is put in the current working directory.
+var_dir: $cwd/var
[paths.fhs]
# Filesystem Hiearchy Standard 2.3
diff --git a/src/mailman/config/schema.cfg b/src/mailman/config/schema.cfg
index c7a63e794..4a896eec5 100644
--- a/src/mailman/config/schema.cfg
+++ b/src/mailman/config/schema.cfg
@@ -59,7 +59,7 @@ pre_hook:
post_hook:
# Which paths.* file system layout to use.
-layout: dev
+layout: here
# Can MIME filtered messages be preserved by list owners?
filtered_messages_are_preservable: no
diff --git a/src/mailman/config/tests/test_archivers.py b/src/mailman/config/tests/test_archivers.py
index 08e466878..b74f680d9 100644
--- a/src/mailman/config/tests/test_archivers.py
+++ b/src/mailman/config/tests/test_archivers.py
@@ -17,9 +17,6 @@
"""Site-wide archiver configuration tests."""
-from __future__ import absolute_import, print_function, unicode_literals
-
-__metaclass__ = type
__all__ = [
'TestArchivers',
]
diff --git a/src/mailman/config/tests/test_configuration.py b/src/mailman/config/tests/test_configuration.py
index f3a49d64f..253b63239 100644
--- a/src/mailman/config/tests/test_configuration.py
+++ b/src/mailman/config/tests/test_configuration.py
@@ -17,9 +17,6 @@
"""Test the system-wide global configuration."""
-from __future__ import absolute_import, print_function, unicode_literals
-
-__metaclass__ = type
__all__ = [
'TestConfiguration',
'TestConfigurationErrors',
@@ -32,6 +29,7 @@ import mock
import tempfile
import unittest
+from contextlib import ExitStack
from mailman.config.config import (
Configuration, external_configuration, load_external)
from mailman.interfaces.configuration import (
@@ -65,26 +63,13 @@ class TestConfiguration(unittest.TestCase):
class TestExternal(unittest.TestCase):
"""Test external configuration file loading APIs."""
- def test_load_external_by_filename_as_bytes(self):
+ def test_load_external_by_filename(self):
filename = resource_filename('mailman.config', 'postfix.cfg')
contents = load_external(filename)
- self.assertIsInstance(contents, bytes)
- self.assertEqual(contents[:9], b'[postfix]')
-
- def test_load_external_by_path_as_bytes(self):
- contents = load_external('python:mailman.config.postfix')
- self.assertIsInstance(contents, bytes)
- self.assertEqual(contents[:9], b'[postfix]')
-
- def test_load_external_by_filename_as_string(self):
- filename = resource_filename('mailman.config', 'postfix.cfg')
- contents = load_external(filename, encoding='utf-8')
- self.assertIsInstance(contents, unicode)
self.assertEqual(contents[:9], '[postfix]')
- def test_load_external_by_path_as_string(self):
- contents = load_external('python:mailman.config.postfix', 'utf-8')
- self.assertIsInstance(contents, unicode)
+ def test_load_external_by_path(self):
+ contents = load_external('python:mailman.config.postfix')
self.assertEqual(contents[:9], '[postfix]')
def test_external_configuration_by_filename(self):
@@ -121,24 +106,32 @@ layout: nonesuch
# Use a fake sys.exit() function that records that it was called, and
# that prevents further processing.
config = Configuration()
- # Suppress warning messages in the test output.
- with self.assertRaises(SystemExit) as cm, mock.patch('sys.stderr'):
+ # Suppress warning messages in the test output. Also, make sure that
+ # the config.load() call doesn't break global state.
+ with ExitStack() as resources:
+ resources.enter_context(mock.patch('sys.stderr'))
+ resources.enter_context(mock.patch.object(config, '_clear'))
+ cm = resources.enter_context(self.assertRaises(SystemExit))
config.load(filename)
self.assertEqual(cm.exception.args, (1,))
def test_path_expansion_infloop(self):
- # A path expansion never completes because it references a
- # non-existent substitution variable.
+ # A path expansion never completes because it references a non-existent
+ # substitution variable.
fd, filename = tempfile.mkstemp()
self.addCleanup(os.remove, filename)
os.close(fd)
with open(filename, 'w') as fp:
print("""\
-[paths.dev]
+[paths.here]
log_dir: $nopath/log_dir
""", file=fp)
config = Configuration()
- # Suppress warning messages in the test output.
- with self.assertRaises(SystemExit) as cm, mock.patch('sys.stderr'):
+ # Suppress warning messages in the test output. Also, make sure that
+ # the config.load() call doesn't break global state.
+ with ExitStack() as resources:
+ resources.enter_context(mock.patch('sys.stderr'))
+ resources.enter_context(mock.patch.object(config, '_clear'))
+ cm = resources.enter_context(self.assertRaises(SystemExit))
config.load(filename)
self.assertEqual(cm.exception.args, (1,))
diff --git a/src/mailman/core/chains.py b/src/mailman/core/chains.py
index df4c199d5..610c396b0 100644
--- a/src/mailman/core/chains.py
+++ b/src/mailman/core/chains.py
@@ -17,21 +17,17 @@
"""Application support for chain processing."""
-from __future__ import absolute_import, print_function, unicode_literals
-
-__metaclass__ = type
__all__ = [
'initialize',
'process',
]
-from zope.interface.verify import verifyObject
-
from mailman.chains.base import Chain, TerminalChainBase
from mailman.config import config
from mailman.interfaces.chain import LinkAction, IChain
from mailman.utilities.modules import find_components
+from zope.interface.verify import verifyObject
diff --git a/src/mailman/core/constants.py b/src/mailman/core/constants.py
index f8e354199..63fa0d0d8 100644
--- a/src/mailman/core/constants.py
+++ b/src/mailman/core/constants.py
@@ -17,21 +17,17 @@
"""Various constants and enumerations."""
-from __future__ import absolute_import, print_function, unicode_literals
-
-__metaclass__ = type
__all__ = [
'system_preferences',
]
-from zope.component import getUtility
-from zope.interface import implementer
-
from mailman.config import config
from mailman.interfaces.languages import ILanguageManager
from mailman.interfaces.member import DeliveryMode, DeliveryStatus
from mailman.interfaces.preferences import IPreferences
+from zope.component import getUtility
+from zope.interface import implementer
diff --git a/src/mailman/core/docs/runner.rst b/src/mailman/core/docs/runner.rst
index e9fd21c57..11a771fe8 100644
--- a/src/mailman/core/docs/runner.rst
+++ b/src/mailman/core/docs/runner.rst
@@ -55,7 +55,7 @@ on instance variables.
... A test message.
... """)
>>> switchboard = config.switchboards['test']
- >>> filebase = switchboard.enqueue(msg, listname=mlist.fqdn_listname,
+ >>> filebase = switchboard.enqueue(msg, listid=mlist.list_id,
... foo='yes', bar='no')
>>> runner.run()
>>> print(runner.msg.as_string())
@@ -69,7 +69,7 @@ on instance variables.
bar : no
foo : yes
lang : en
- listname : test@example.com
+ listid : test.example.com
version : 3
XXX More of the Runner API should be tested.
diff --git a/src/mailman/core/errors.py b/src/mailman/core/errors.py
index b8f5a1f64..95b1ae821 100644
--- a/src/mailman/core/errors.py
+++ b/src/mailman/core/errors.py
@@ -26,9 +26,6 @@ interfaces.
"""
-from __future__ import absolute_import, print_function, unicode_literals
-
-__metaclass__ = type
__all__ = [
'AlreadyReceivingDigests',
'AlreadyReceivingRegularDeliveries',
diff --git a/src/mailman/core/i18n.py b/src/mailman/core/i18n.py
index b078a985f..ae9dcc8b8 100644
--- a/src/mailman/core/i18n.py
+++ b/src/mailman/core/i18n.py
@@ -17,9 +17,6 @@
"""Internationalization."""
-from __future__ import absolute_import, print_function, unicode_literals
-
-__metaclass__ = type
__all__ = [
'_',
'ctime',
@@ -28,11 +25,12 @@ __all__ = [
import time
-from flufl.i18n import PackageStrategy, registry
-
import mailman.messages
+
+from flufl.i18n import PackageStrategy, registry
from mailman.interfaces.configuration import ConfigurationUpdatedEvent
+
_ = None
diff --git a/src/mailman/core/initialize.py b/src/mailman/core/initialize.py
index 3a0e27024..47d7106e2 100644
--- a/src/mailman/core/initialize.py
+++ b/src/mailman/core/initialize.py
@@ -24,9 +24,6 @@ line argument parsing, since some of the initialization behavior is controlled
by the command line arguments.
"""
-from __future__ import absolute_import, print_function, unicode_literals
-
-__metaclass__ = type
__all__ = [
'initialize',
'initialize_1',
@@ -38,16 +35,15 @@ __all__ = [
import os
import sys
-
-from pkg_resources import resource_string
-from zope.component import getUtility
-from zope.configuration import xmlconfig
-
import mailman.config.config
import mailman.core.logging
from mailman.interfaces.database import IDatabaseFactory
from mailman.utilities.modules import call_name
+from pkg_resources import resource_string as resource_bytes
+from zope.component import getUtility
+from zope.configuration import xmlconfig
+
# The test infrastructure uses this to prevent the search and loading of any
# existing configuration file. Otherwise the existence of say a
@@ -109,8 +105,8 @@ def initialize_1(config_path=None):
:param config_path: The path to the configuration file.
:type config_path: string
"""
- zcml = resource_string('mailman.config', 'configure.zcml')
- xmlconfig.string(zcml)
+ zcml = resource_bytes('mailman.config', 'configure.zcml')
+ xmlconfig.string(zcml.decode('utf-8'))
# 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
diff --git a/src/mailman/core/logging.py b/src/mailman/core/logging.py
index c5ce1a538..7529cc1d7 100644
--- a/src/mailman/core/logging.py
+++ b/src/mailman/core/logging.py
@@ -17,9 +17,6 @@
"""Logging initialization, using Python's standard logging package."""
-from __future__ import absolute_import, print_function, unicode_literals
-
-__metaclass__ = type
__all__ = [
'initialize',
'reopen',
diff --git a/src/mailman/core/pipelines.py b/src/mailman/core/pipelines.py
index e164169a4..b7773736c 100644
--- a/src/mailman/core/pipelines.py
+++ b/src/mailman/core/pipelines.py
@@ -17,9 +17,6 @@
"""Built-in pipelines."""
-from __future__ import absolute_import, print_function, unicode_literals
-
-__metaclass__ = type
__all__ = [
'BasePipeline',
'OwnerPipeline',
@@ -32,9 +29,6 @@ __all__ = [
import logging
-from zope.interface import implementer
-from zope.interface.verify import verifyObject
-
from mailman.app.bounces import bounce_message
from mailman.config import config
from mailman.core import errors
@@ -42,6 +36,8 @@ from mailman.core.i18n import _
from mailman.interfaces.handler import IHandler
from mailman.interfaces.pipeline import IPipeline
from mailman.utilities.modules import find_components
+from zope.interface import implementer
+from zope.interface.verify import verifyObject
dlog = logging.getLogger('mailman.debug')
@@ -120,6 +116,7 @@ class PostingPipeline(BasePipeline):
'cleanse',
'cleanse-dkim',
'cook-headers',
+ 'subject-prefix',
'rfc-2369',
'to-archive',
'to-digest',
diff --git a/src/mailman/core/rules.py b/src/mailman/core/rules.py
index 1a2b9f56d..0110c07f7 100644
--- a/src/mailman/core/rules.py
+++ b/src/mailman/core/rules.py
@@ -17,19 +17,15 @@
"""Various rule helpers"""
-from __future__ import absolute_import, print_function, unicode_literals
-
-__metaclass__ = type
__all__ = [
'initialize',
]
-from zope.interface.verify import verifyObject
-
from mailman.config import config
from mailman.interfaces.rules import IRule
from mailman.utilities.modules import find_components
+from zope.interface.verify import verifyObject
diff --git a/src/mailman/core/runner.py b/src/mailman/core/runner.py
index 81a2ea3d1..5ffc3f57d 100644
--- a/src/mailman/core/runner.py
+++ b/src/mailman/core/runner.py
@@ -17,9 +17,6 @@
"""The process runner base class."""
-from __future__ import absolute_import, print_function, unicode_literals
-
-__metaclass__ = type
__all__ = [
'Runner',
]
@@ -30,12 +27,7 @@ import signal
import logging
import traceback
-from cStringIO import StringIO
from lazr.config import as_boolean, as_timedelta
-from zope.component import getUtility
-from zope.event import notify
-from zope.interface import implementer
-
from mailman.config import config
from mailman.core.i18n import _
from mailman.core.logging import reopen
@@ -44,6 +36,10 @@ from mailman.interfaces.languages import ILanguageManager
from mailman.interfaces.listmanager import IListManager
from mailman.interfaces.runner import IRunner, RunnerCrashEvent
from mailman.utilities.string import expand
+from six.moves import cStringIO as StringIO
+from zope.component import getUtility
+from zope.event import notify
+from zope.interface import implementer
dlog = logging.getLogger('mailman.debug')
@@ -218,16 +214,26 @@ class Runner:
# them out of our sight.
#
# Find out which mailing list this message is destined for.
+ mlist = None
missing = object()
- listname = msgdata.get('listname', missing)
- mlist = (None
- if listname is missing
- else getUtility(IListManager).get(unicode(listname)))
+ # First try to dig out the target list by id. If there's no list-id
+ # in the metadata, fall back to the fqdn list name for backward
+ # compatibility.
+ list_manager = getUtility(IListManager)
+ list_id = msgdata.get('listid', missing)
+ fqdn_listname = None
+ if list_id is missing:
+ fqdn_listname = msgdata.get('listname', missing)
+ # XXX Deprecate.
+ if fqdn_listname is not missing:
+ mlist = list_manager.get(fqdn_listname)
+ else:
+ mlist = list_manager.get_by_list_id(list_id)
if mlist is None:
+ identifier = (list_id if list_id is not None else fqdn_listname)
elog.error(
'%s runner "%s" shunting message for missing list: %s',
- msg['message-id'], self.name,
- ('n/a' if listname is missing else listname))
+ msg['message-id'], self.name, identifier)
config.switchboards['shunt'].enqueue(msg, msgdata)
return
# Now process this message. We also want to set up the language
diff --git a/src/mailman/core/switchboard.py b/src/mailman/core/switchboard.py
index 2e8ef24a7..f54bc243a 100644
--- a/src/mailman/core/switchboard.py
+++ b/src/mailman/core/switchboard.py
@@ -24,9 +24,6 @@ written. First, the message is written to the pickle, then the metadata
dictionary is written.
"""
-from __future__ import absolute_import, print_function, unicode_literals
-
-__metaclass__ = type
__all__ = [
'Switchboard',
'handle_ConfigurationUpdatedEvent',
@@ -37,22 +34,22 @@ import os
import time
import email
import pickle
-import cPickle
import hashlib
import logging
-from zope.interface import implementer
-
from mailman.config import config
from mailman.email.message import Message
from mailman.interfaces.configuration import ConfigurationUpdatedEvent
from mailman.interfaces.switchboard import ISwitchboard
from mailman.utilities.filesystem import makedirs
from mailman.utilities.string import expand
+from six.moves import cPickle
+from zope.interface import implementer
-# 20 bytes of all bits set, maximum hashlib.sha.digest() value.
-shamax = 0xffffffffffffffffffffffffffffffffffffffffL
+# 20 bytes of all bits set, maximum hashlib.sha.digest() value. We do it this
+# way for Python 2/3 compatibility.
+shamax = int('0xffffffffffffffffffffffffffffffffffffffff', 16)
# Small increment to add to time in case two entries have the same time. This
# prevents skipping one of two entries with the same time until the next pass.
DELTA = .0001
@@ -92,7 +89,7 @@ class Switchboard:
self.queue_directory = queue_directory
# If configured to, create the directory if it doesn't yet exist.
if config.create_paths:
- makedirs(self.queue_directory, 0770)
+ makedirs(self.queue_directory, 0o770)
# Fast track for no slices
self._lower = None
self._upper = None
@@ -112,37 +109,37 @@ class Switchboard:
# of parallel runner processes.
data = _metadata.copy()
data.update(_kws)
- listname = data.get('listname', '--nolist--')
+ list_id = data.get('listid', '--nolist--')
# Get some data for the input to the sha hash.
- now = time.time()
+ now = repr(time.time())
if data.get('_plaintext'):
protocol = 0
msgsave = cPickle.dumps(str(_msg), protocol)
else:
protocol = pickle.HIGHEST_PROTOCOL
msgsave = cPickle.dumps(_msg, protocol)
- # listname is unicode but the input to the hash function must be an
- # 8-bit string (eventually, a bytes object).
- hashfood = msgsave + listname.encode('utf-8') + repr(now)
+ # The list-id field is a string but the input to the hash function must
+ # be bytes.
+ hashfood = msgsave + list_id.encode('utf-8') + now.encode('utf-8')
# Encode the current time into the file name for FIFO sorting. The
# file name consists of two parts separated by a '+': the received
# time for this message (i.e. when it first showed up on this system)
# and the sha hex digest.
- filebase = repr(now) + '+' + hashlib.sha1(hashfood).hexdigest()
+ filebase = now + '+' + hashlib.sha1(hashfood).hexdigest()
filename = os.path.join(self.queue_directory, filebase + '.pck')
tmpfile = filename + '.tmp'
# Always add the metadata schema version number
data['version'] = config.QFILE_SCHEMA_VERSION
# Filter out volatile entries. Use .keys() so that we can mutate the
# dictionary during the iteration.
- for k in data.keys():
+ for k in list(data):
if k.startswith('_'):
del data[k]
# We have to tell the dequeue() method whether to parse the message
# object or not.
data['_parsemsg'] = (protocol == 0)
# Write to the pickle file the message object and metadata.
- with open(tmpfile, 'w') as fp:
+ with open(tmpfile, 'wb') as fp:
fp.write(msgsave)
cPickle.dump(data, fp, protocol)
fp.flush()
@@ -156,7 +153,7 @@ class Switchboard:
filename = os.path.join(self.queue_directory, filebase + '.pck')
backfile = os.path.join(self.queue_directory, filebase + '.bak')
# Read the message object and metadata.
- with open(filename) as fp:
+ with open(filename, 'rb') as fp:
# Move the file to the backup file name for processing. If this
# process crashes uncleanly the .bak file will be used to
# re-instate the .pck file in order to try again.
@@ -207,13 +204,13 @@ class Switchboard:
# Throw out any files which don't match our bitrange. BAW: test
# performance and end-cases of this algorithm. MAS: both
# comparisons need to be <= to get complete range.
- if lower is None or (lower <= long(digest, 16) <= upper):
+ if lower is None or (lower <= int(digest, 16) <= upper):
key = float(when)
while key in times:
key += DELTA
times[key] = filebase
# FIFO sort
- return [times[key] for key in sorted(times)]
+ return [times[k] for k in sorted(times)]
def recover_backup_files(self):
"""See `ISwitchboard`."""
@@ -228,7 +225,8 @@ class Switchboard:
dst = os.path.join(self.queue_directory, filebase + '.pck')
with open(src, 'rb+') as fp:
try:
- msg = cPickle.load(fp)
+ # Throw away the message object.
+ cPickle.load(fp)
data_pos = fp.tell()
data = cPickle.load(fp)
except Exception as error:
diff --git a/src/mailman/core/system.py b/src/mailman/core/system.py
index 495cc37ee..0c01d94aa 100644
--- a/src/mailman/core/system.py
+++ b/src/mailman/core/system.py
@@ -17,9 +17,6 @@
"""System information."""
-from __future__ import absolute_import, print_function, unicode_literals
-
-__metaclass__ = type
__all__ = [
'system',
]
@@ -27,10 +24,9 @@ __all__ = [
import sys
-from zope.interface import implementer
-
from mailman import version
from mailman.interfaces.system import ISystem
+from zope.interface import implementer
diff --git a/src/mailman/core/tests/test_pipelines.py b/src/mailman/core/tests/test_pipelines.py
index 67e6af36e..91be1f79f 100644
--- a/src/mailman/core/tests/test_pipelines.py
+++ b/src/mailman/core/tests/test_pipelines.py
@@ -17,9 +17,6 @@
"""Test the core modification pipelines."""
-from __future__ import absolute_import, print_function, unicode_literals
-
-__metaclass__ = type
__all__ = [
'TestOwnerPipeline',
'TestPostingPipeline',
@@ -28,9 +25,6 @@ __all__ = [
import unittest
-from zope.component import getUtility
-from zope.interface import implementer
-
from mailman.app.lifecycle import create_list
from mailman.config import config
from mailman.core.errors import DiscardMessage, RejectMessage
@@ -40,11 +34,11 @@ from mailman.interfaces.member import MemberRole
from mailman.interfaces.pipeline import IPipeline
from mailman.interfaces.usermanager import IUserManager
from mailman.testing.helpers import (
- LogFileMark,
- get_queue_messages,
- reset_the_world,
+ LogFileMark, get_queue_messages, reset_the_world,
specialized_message_from_string as mfs)
from mailman.testing.layers import ConfigLayer
+from zope.component import getUtility
+from zope.interface import implementer
@@ -175,5 +169,5 @@ To: test-owner@example.com
pipeline_name='default-owner-pipeline')
messages = get_queue_messages('out', sort_on='to')
self.assertEqual(len(messages), 1)
- self.assertEqual(messages[0].msgdata['recipients'],
+ self.assertEqual(messages[0].msgdata['recipients'],
set(('anne@example.com', 'bart@example.com')))
diff --git a/src/mailman/core/tests/test_runner.py b/src/mailman/core/tests/test_runner.py
index 1fb8f0b7b..3d2e76096 100644
--- a/src/mailman/core/tests/test_runner.py
+++ b/src/mailman/core/tests/test_runner.py
@@ -17,9 +17,6 @@
"""Test some Runner base class behavior."""
-from __future__ import absolute_import, print_function, unicode_literals
-
-__metaclass__ = type
__all__ = [
'TestRunner',
]
@@ -70,7 +67,7 @@ To: test@example.com
Message-ID: <ant>
""")
- config.switchboards['in'].enqueue(msg, listname='test@example.com')
+ config.switchboards['in'].enqueue(msg, listid='test.example.com')
with event_subscribers(self._got_event):
runner.run()
# We should now have exactly one event, which will contain the
@@ -81,7 +78,7 @@ Message-ID: <ant>
self.assertTrue(isinstance(event, RunnerCrashEvent))
self.assertEqual(event.mailing_list, self._mlist)
self.assertEqual(event.message['message-id'], '<ant>')
- self.assertEqual(event.metadata['listname'], 'test@example.com')
+ self.assertEqual(event.metadata['listid'], 'test.example.com')
self.assertTrue(isinstance(event.error, RuntimeError))
self.assertEqual(str(event.error), 'borked')
self.assertTrue(isinstance(event.runner, CrashingRunner))
diff --git a/src/mailman/database/alembic/__init__.py b/src/mailman/database/alembic/__init__.py
index ffd3af6df..4dbbc31d9 100644
--- a/src/mailman/database/alembic/__init__.py
+++ b/src/mailman/database/alembic/__init__.py
@@ -17,9 +17,6 @@
"""Alembic configuration initization."""
-from __future__ import absolute_import, print_function, unicode_literals
-
-__metaclass__ = type
__all__ = [
'alembic_cfg',
]
diff --git a/src/mailman/database/alembic/env.py b/src/mailman/database/alembic/env.py
index 125868566..261782d29 100644
--- a/src/mailman/database/alembic/env.py
+++ b/src/mailman/database/alembic/env.py
@@ -17,9 +17,6 @@
"""Alembic migration environment."""
-from __future__ import absolute_import, print_function, unicode_literals
-
-__metaclass__ = type
__all__ = [
'run_migrations_offline',
'run_migrations_online',
@@ -28,11 +25,10 @@ __all__ = [
from alembic import context
from contextlib import closing
-from sqlalchemy import create_engine
-
from mailman.config import config
from mailman.database.model import Model
from mailman.utilities.string import expand
+from sqlalchemy import create_engine
diff --git a/src/mailman/database/alembic/versions/51b7f92bd06c_initial.py b/src/mailman/database/alembic/versions/51b7f92bd06c_initial.py
index 3feb24fff..5e3527abe 100644
--- a/src/mailman/database/alembic/versions/51b7f92bd06c_initial.py
+++ b/src/mailman/database/alembic/versions/51b7f92bd06c_initial.py
@@ -29,9 +29,6 @@ Revises: None
Create Date: 2014-10-10 09:53:35.624472
"""
-from __future__ import absolute_import, print_function, unicode_literals
-
-__metaclass__ = type
__all__ = [
'downgrade',
'upgrade',
diff --git a/src/mailman/database/base.py b/src/mailman/database/base.py
index 55edf6005..09fd47b80 100644
--- a/src/mailman/database/base.py
+++ b/src/mailman/database/base.py
@@ -15,9 +15,8 @@
# You should have received a copy of the GNU General Public License along with
# GNU Mailman. If not, see <http://www.gnu.org/licenses/>.
-from __future__ import absolute_import, print_function, unicode_literals
+"""Common database support."""
-__metaclass__ = type
__all__ = [
'SABaseDatabase',
]
@@ -25,17 +24,15 @@ __all__ = [
import logging
-from sqlalchemy import create_engine
-from sqlalchemy.orm import sessionmaker
-from zope.interface import implementer
-
from mailman.config import config
from mailman.interfaces.database import IDatabase
from mailman.utilities.string import expand
+from sqlalchemy import create_engine
+from sqlalchemy.orm import sessionmaker
+from zope.interface import implementer
log = logging.getLogger('mailman.database')
-NL = '\n'
diff --git a/src/mailman/database/factory.py b/src/mailman/database/factory.py
index 64174449d..9fffd4545 100644
--- a/src/mailman/database/factory.py
+++ b/src/mailman/database/factory.py
@@ -17,9 +17,6 @@
"""Database factory."""
-from __future__ import absolute_import, print_function, unicode_literals
-
-__metaclass__ = type
__all__ = [
'DatabaseFactory',
'DatabaseTestingFactory',
@@ -33,16 +30,15 @@ import alembic.command
from alembic.migration import MigrationContext
from alembic.script import ScriptDirectory
from flufl.lock import Lock
-from sqlalchemy import MetaData
-from zope.interface import implementer
-from zope.interface.verify import verifyObject
-
from mailman.config import config
from mailman.database.alembic import alembic_cfg
from mailman.database.model import Model
from mailman.interfaces.database import (
DatabaseError, IDatabase, IDatabaseFactory)
from mailman.utilities.modules import call_name
+from sqlalchemy import MetaData
+from zope.interface import implementer
+from zope.interface.verify import verifyObject
LAST_STORM_SCHEMA_VERSION = '20130406000000'
diff --git a/src/mailman/database/model.py b/src/mailman/database/model.py
index a6056bf63..8dad6f0cf 100644
--- a/src/mailman/database/model.py
+++ b/src/mailman/database/model.py
@@ -17,9 +17,6 @@
"""Base class for all database classes."""
-from __future__ import absolute_import, print_function, unicode_literals
-
-__metaclass__ = type
__all__ = [
'Model',
]
diff --git a/src/mailman/database/postgresql.py b/src/mailman/database/postgresql.py
index 717b69dd1..4a6f02da6 100644
--- a/src/mailman/database/postgresql.py
+++ b/src/mailman/database/postgresql.py
@@ -17,9 +17,6 @@
"""PostgreSQL database support."""
-from __future__ import absolute_import, print_function, unicode_literals
-
-__metaclass__ = type
__all__ = [
'PostgreSQLDatabase',
]
diff --git a/src/mailman/database/sqlite.py b/src/mailman/database/sqlite.py
index db7860390..95dba460e 100644
--- a/src/mailman/database/sqlite.py
+++ b/src/mailman/database/sqlite.py
@@ -17,9 +17,6 @@
"""SQLite database support."""
-from __future__ import absolute_import, print_function, unicode_literals
-
-__metaclass__ = type
__all__ = [
'SQLiteDatabase',
]
@@ -28,7 +25,7 @@ __all__ = [
import os
from mailman.database.base import SABaseDatabase
-from urlparse import urlparse
+from six.moves.urllib_parse import urlparse
diff --git a/src/mailman/database/tests/test_factory.py b/src/mailman/database/tests/test_factory.py
index 29cca41ba..71f810a56 100644
--- a/src/mailman/database/tests/test_factory.py
+++ b/src/mailman/database/tests/test_factory.py
@@ -17,9 +17,6 @@
"""Test database schema migrations"""
-from __future__ import absolute_import, print_function, unicode_literals
-
-__metaclass__ = type
__all__ = [
'TestSchemaManager',
]
@@ -28,17 +25,16 @@ __all__ = [
import unittest
import alembic.command
-from mock import patch
-from sqlalchemy import MetaData, Table, Column, Integer, Unicode
-from sqlalchemy.exc import ProgrammingError, OperationalError
-from sqlalchemy.schema import Index
-
from mailman.config import config
from mailman.database.alembic import alembic_cfg
from mailman.database.factory import LAST_STORM_SCHEMA_VERSION, SchemaManager
from mailman.database.model import Model
from mailman.interfaces.database import DatabaseError
from mailman.testing.layers import ConfigLayer
+from mock import patch
+from sqlalchemy import MetaData, Table, Column, Integer, Unicode
+from sqlalchemy.exc import ProgrammingError, OperationalError
+from sqlalchemy.schema import Index
diff --git a/src/mailman/database/transaction.py b/src/mailman/database/transaction.py
index 3e156cfb8..dc468aaab 100644
--- a/src/mailman/database/transaction.py
+++ b/src/mailman/database/transaction.py
@@ -17,9 +17,6 @@
"""Transactional support."""
-from __future__ import absolute_import, print_function, unicode_literals
-
-__metaclass__ = type
__all__ = [
'dbconnection',
'transaction',
@@ -28,7 +25,6 @@ __all__ = [
from contextlib import contextmanager
-
from mailman.config import config
diff --git a/src/mailman/database/types.py b/src/mailman/database/types.py
index 1984b08b5..463d271f0 100644
--- a/src/mailman/database/types.py
+++ b/src/mailman/database/types.py
@@ -15,17 +15,14 @@
# You should have received a copy of the GNU General Public License along with
# GNU Mailman. If not, see <http://www.gnu.org/licenses/>.
-"""Storm type conversions."""
+"""Database type conversions."""
-
-from __future__ import absolute_import, print_function, unicode_literals
-
-__metaclass__ = type
__all__ = [
'Enum',
'UUID',
]
+
import uuid
from sqlalchemy import Integer
diff --git a/src/mailman/docs/DEVELOP.rst b/src/mailman/docs/DEVELOP.rst
index c9e1bd596..f1225658e 100644
--- a/src/mailman/docs/DEVELOP.rst
+++ b/src/mailman/docs/DEVELOP.rst
@@ -3,7 +3,9 @@ Developing Mailman
==================
The following documentation is generated from the internal developer
-documentation. This documentation is also used by the test suite.
+documentation. This documentation is also used by the test suite. Another
+good source of architectural information is available in the chapter written
+by Barry Warsaw for the `Architecture of Open Source Applications`_.
For now, this will have to suffice as an overview of the Mailman system.
@@ -154,3 +156,4 @@ extensive set of command line commands, and email commands.
.. _`Python pickles`: http://docs.python.org/2/library/pickle.html
+.. _`Architecture of Open Source Applications`: http://www.aosabook.org/en/mailman.html
diff --git a/src/mailman/docs/INTRODUCTION.rst b/src/mailman/docs/INTRODUCTION.rst
index ac77d0e72..b4f016c46 100644
--- a/src/mailman/docs/INTRODUCTION.rst
+++ b/src/mailman/docs/INTRODUCTION.rst
@@ -82,7 +82,7 @@ lists and archives, etc., are available at:
Requirements
============
-Mailman 3.0 requires `Python 2.7`_.
+Mailman 3 requires `Python 3.4`_ or newer.
.. _`GNU Mailman`: http://www.list.org
@@ -90,4 +90,4 @@ Mailman 3.0 requires `Python 2.7`_.
.. _`Getting Started`: START.html
.. _Python: http://www.python.org
.. _FAQ: http://wiki.list.org/display/DOC/Frequently+Asked+Questions
-.. _`Python 2.7`: http://www.python.org/download/releases/2.7.3/
+.. _`Python 3.4`: https://www.python.org/downloads/release/python-342/
diff --git a/src/mailman/docs/NEWS.rst b/src/mailman/docs/NEWS.rst
index 651e4b98f..9115c9bdb 100644
--- a/src/mailman/docs/NEWS.rst
+++ b/src/mailman/docs/NEWS.rst
@@ -12,6 +12,22 @@ Here is a history of user visible changes to Mailman.
===============================
(2015-XX-XX)
+Configuration
+-------------
+ * When specifying a file system path in the [paths.*] section, $cfg_file can
+ be used to expand into the path of the ``-C`` option if given. In the
+ default ``[paths.dev]`` section, ``$var_dir`` is now specified relative to
+ ``$cfg_file`` so that it won't accidentally be relative to the current
+ working directory, if ``-C`` is given.
+ * ``$cwd`` is now an additional substitution variable for the ``mailman.cfg``
+ file's ``[paths.*]`` sections. A new ``[paths.here]`` section is added,
+ which puts the ``var_dir`` in ``$cwd``. It is made the default layout.
+
+REST
+----
+ * You can now view the contents of, inject messages into, and delete messages
+ from the various queue directories via the ``<api>/queues`` resource.
+
3.0 beta 5 -- "Carve Away The Stone"
====================================
@@ -49,6 +65,7 @@ Database
Development
-----------
+ * Python 3.4 is now the minimum requirement.
* You no longer have to create a virtual environment separately when running
the test suite. Just use `tox`.
* You no longer have to edit `src/mailman/testing/testing.cfg` to run the
diff --git a/src/mailman/docs/START.rst b/src/mailman/docs/START.rst
index 794740c64..454f6a387 100644
--- a/src/mailman/docs/START.rst
+++ b/src/mailman/docs/START.rst
@@ -39,12 +39,11 @@ list, or ask on IRC channel ``#mailman`` on Freenode.
Requirements
============
-Python 2.7 is required. It can either be the default 'python' on your
-``$PATH`` or it can be accessible via the ``python2.7`` binary. If
-your operating system does not include Python, see http://www.python.org
-for information about downloading installers (where available) and
-installing it from source (when necessary or preferred). Python 3 is
-not yet supported.
+Python 3.4 or newer is required. It can either be the default 'python3' on
+your ``$PATH`` or it can be accessible via the ``python3.4`` binary. If your
+operating system does not include Python, see http://www.python.org for
+information about downloading installers (where available) and installing it
+from source (when necessary or preferred). Python 2 is not supported.
You may need some additional dependencies, which are either available from
your OS vendor, or can be downloaded automatically from the `Python
@@ -80,9 +79,9 @@ downloads everything from the Cheeseshop.
You do have access to the virtualenv, and you can use this to run individual
tests, e.g.::
- $ .tox/py27/bin/python -m nose2 -vv -P user
+ $ .tox/py34/bin/python -m nose2 -vv -P user
-Use `.tox/py27/bin/python -m nose2 --help` for more options.
+Use `.tox/py34/bin/python -m nose2 --help` for more options.
If you want to run the full test suite against the PostgreSQL database, set
the database up as described in :doc:`DATABASE`, then create a `postgres.cfg`
@@ -112,23 +111,23 @@ installed.
First, create a virtual environment. By default ``virtualenv`` uses the
``python`` executable it finds first on your ``$PATH``. Make sure this is
-Python 2.7 (just start the interactive interpreter and check the version in
+Python 3.4 (just start the interactive interpreter and check the version in
the startup banner). The directory you install the virtualenv into is up to
-you, but for purposes of this document, we'll install it into ``/tmp/py27``::
+you, but for purposes of this document, we'll install it into ``/tmp/mm3``::
- % virtualenv --system-site-packages /tmp/py27
+ % virtualenv -p python3 --system-site-packages /tmp/mm3
-If your default Python is not version 2.7, use the ``--python`` option to
+If your default Python is not version 3.4, use the ``--python`` option to
specify the Python executable. You can use the command name if this version
is on your ``PATH``::
- % virtualenv --system-site-packages --python=python2.7 /tmp/py27
+ % virtualenv --system-site-packages --python=python3.4 /tmp/mm3
-or you may specify the full path to any Python 2.7 executable.
+or you may specify the full path to any Python 3.4 executable.
Now, activate the virtual environment and set it up for development::
- % source /tmp/py27/bin/activate
+ % source /tmp/mm3/bin/activate
% python setup.py develop
Sit back and have some Kombucha while you wait for everything to download and
diff --git a/src/mailman/docs/STYLEGUIDE.rst b/src/mailman/docs/STYLEGUIDE.rst
index 13fb0cdf1..1d63d2b46 100644
--- a/src/mailman/docs/STYLEGUIDE.rst
+++ b/src/mailman/docs/STYLEGUIDE.rst
@@ -15,33 +15,25 @@ http://barry.warsaw.us/software/STYLEGUIDE.txt
This document contains a style guide for Python programming, as used in GNU
Mailman. `PEP 8`_ is the basis for this style guide so it's recommendations
should be followed except for the differences outlined here. This document
-assumes the use of Python 2.7, but not (yet) Python 3.
+assumes the use of Python 3.
-* After file comments (e.g. license block), add a ``__metaclass__`` definition
- so that all classes will be new-style. Following that, add an ``__all__``
- section that names, one-per-line, all the public names exported by this
- module. You should enable absolute imports and unicode literals. See the
+* After file comments (e.g. license block), add an ``__all__`` section that
+ names, one-per-line, all the public names exported by this module. See the
`GNU Mailman Python template`_ as an example.
* Imports are always put at the top of the file, just after any module
comments and docstrings, and before module globals and constants, but after
- any ``__future__`` imports, or ``__metaclass__`` and ``__all__``
- definitions.
+ any ``__all__`` definitions.
Imports should be grouped, with the order being:
- 1. non-from imports for standard and third party libraries
- 2. non-from imports from the application
- 3. from-imports from the standard and third party libraries
- 4. from-imports from the application
+ 1. non-from imports, grouped from shorted module name to longest module
+ name, with ties being broken by alphabetical order.
+ 3. from-imports grouped alphabetically.
- From-imports should follow non-from imports. Dotted imports should follow
- non-dotted imports. Non-dotted imports should be grouped by increasing
- length, while dotted imports should be grouped alphabetically.
-
-* In general, there should be one class per module. Keep files small, but
- it's okay to group related code together. List everything exported from the
- module in the ``__all__``.
+* In general, there should be one class per module. This is not a
+ hard-and-fast rule. Keep files small, but it's okay to group related code
+ together. List everything exported from the module in the ``__all__``.
* Right hanging comments are discouraged, in favor of preceding comments.
E.g. bad::
diff --git a/src/mailman/docs/__init__.py b/src/mailman/docs/__init__.py
index f588eb14d..fa09dde76 100644
--- a/src/mailman/docs/__init__.py
+++ b/src/mailman/docs/__init__.py
@@ -17,9 +17,6 @@
"""General Mailman doc tests."""
-from __future__ import absolute_import, print_function, unicode_literals
-
-__metaclass__ = type
__all__ = [
'layer',
]
diff --git a/src/mailman/email/message.py b/src/mailman/email/message.py
index e653133ba..d4b373bea 100644
--- a/src/mailman/email/message.py
+++ b/src/mailman/email/message.py
@@ -23,9 +23,6 @@ safe pickle deserialization, even if the email package adds additional Message
attributes.
"""
-from __future__ import absolute_import, print_function, unicode_literals
-
-__metaclass__ = type
__all__ = [
'Message',
'MultipartDigestMessage',
@@ -40,7 +37,6 @@ import email.utils
from email.header import Header
from email.mime.multipart import MIMEMultipart
-
from mailman.config import config
@@ -149,8 +145,8 @@ class UserNotification(Message):
subject = ('(no subject)' if subject is None else subject)
if text is not None:
self.set_payload(text.encode(charset), charset)
- self['Subject'] = Header(subject.encode(charset), charset,
- header_name='Subject', errors='replace')
+ self['Subject'] = Header(
+ subject, charset, header_name='Subject', errors='replace')
self['From'] = sender
if isinstance(recipients, (list, set, tuple)):
self['To'] = COMMASPACE.join(recipients)
@@ -198,7 +194,7 @@ class UserNotification(Message):
reduced_list_headers=True,
)
if mlist is not None:
- enqueue_kws['listname'] = mlist.fqdn_listname
+ enqueue_kws['listid'] = mlist.list_id
enqueue_kws.update(_kws)
virginq.enqueue(self, **enqueue_kws)
@@ -227,7 +223,7 @@ class OwnerNotification(UserNotification):
virginq = config.switchboards['virgin']
# The message metadata better have a `recip' attribute
virginq.enqueue(self,
- listname=mlist.fqdn_listname,
+ listid=mlist.list_id,
recipients=self.recipients,
nodecorate=True,
reduced_list_headers=True,
diff --git a/src/mailman/email/tests/test_message.py b/src/mailman/email/tests/test_message.py
index 1fdef5e86..59335b890 100644
--- a/src/mailman/email/tests/test_message.py
+++ b/src/mailman/email/tests/test_message.py
@@ -17,9 +17,6 @@
"""Test the message API."""
-from __future__ import absolute_import, print_function, unicode_literals
-
-__metaclass__ = type
__all__ = [
'TestMessage',
'TestMessageSubclass',
@@ -27,8 +24,8 @@ __all__ = [
import unittest
-from email.parser import FeedParser
+from email.parser import FeedParser
from mailman.app.lifecycle import create_list
from mailman.email.message import Message, UserNotification
from mailman.testing.helpers import get_queue_messages
@@ -66,7 +63,7 @@ class TestMessage(unittest.TestCase):
class TestMessageSubclass(unittest.TestCase):
def test_i18n_filenames(self):
parser = FeedParser(_factory=Message)
- parser.feed(b"""\
+ parser.feed("""\
Message-ID: <blah@example.com>
Content-Type: multipart/mixed; boundary="------------050607040206050605060208"
@@ -88,6 +85,6 @@ Test content
attachment = msg.get_payload(1)
try:
filename = attachment.get_filename()
- except TypeError as e:
- self.fail(e)
+ except TypeError as error:
+ self.fail(error)
self.assertEqual(filename, u'd\xe9jeuner.txt')
diff --git a/src/mailman/email/validate.py b/src/mailman/email/validate.py
index b4cf8b5e2..d6f664b01 100644
--- a/src/mailman/email/validate.py
+++ b/src/mailman/email/validate.py
@@ -17,9 +17,6 @@
"""Email address validation."""
-from __future__ import absolute_import, print_function, unicode_literals
-
-__metaclass__ = type
__all__ = [
'Validator',
]
@@ -27,11 +24,10 @@ __all__ = [
import re
-from zope.interface import implementer
-
from mailman.interfaces.address import (
IEmailValidator, InvalidEmailAddressError)
from mailman.utilities.email import split_email
+from zope.interface import implementer
# What other characters should be disallowed?
diff --git a/src/mailman/handlers/acknowledge.py b/src/mailman/handlers/acknowledge.py
index c3af9ab27..c10043981 100644
--- a/src/mailman/handlers/acknowledge.py
+++ b/src/mailman/handlers/acknowledge.py
@@ -20,23 +20,19 @@
This only happens if the sender has set their AcknowledgePosts attribute.
"""
-from __future__ import absolute_import, print_function, unicode_literals
-
-__metaclass__ = type
__all__ = [
'Acknowledge',
]
-from zope.component import getUtility
-from zope.interface import implementer
-
from mailman.core.i18n import _
from mailman.email.message import UserNotification
from mailman.interfaces.handler import IHandler
from mailman.interfaces.languages import ILanguageManager
from mailman.utilities.i18n import make
from mailman.utilities.string import oneline
+from zope.component import getUtility
+from zope.interface import implementer
@@ -67,14 +63,13 @@ class Acknowledge:
language = (language_manager[msgdata['lang']]
if 'lang' in msgdata
else member.preferred_language)
- charset = language_manager[language.code].charset
# Now get the acknowledgement template.
display_name = mlist.display_name
text = make('postack.txt',
mailing_list=mlist,
language=language.code,
wrap=False,
- subject=oneline(original_subject, charset),
+ subject=oneline(original_subject, in_unicode=True),
list_name=mlist.list_name,
display_name=display_name,
listinfo_url=mlist.script_url('listinfo'),
diff --git a/src/mailman/handlers/after_delivery.py b/src/mailman/handlers/after_delivery.py
index 7fa7a4554..464fafd8c 100644
--- a/src/mailman/handlers/after_delivery.py
+++ b/src/mailman/handlers/after_delivery.py
@@ -17,19 +17,15 @@
"""Perform some bookkeeping after a successful post."""
-from __future__ import absolute_import, print_function, unicode_literals
-
-__metaclass__ = type
__all__ = [
'AfterDelivery',
]
-from zope.interface import implementer
-
from mailman.core.i18n import _
from mailman.interfaces.handler import IHandler
from mailman.utilities.datetime import now
+from zope.interface import implementer
diff --git a/src/mailman/handlers/avoid_duplicates.py b/src/mailman/handlers/avoid_duplicates.py
index 529a99f68..636a9f24d 100644
--- a/src/mailman/handlers/avoid_duplicates.py
+++ b/src/mailman/handlers/avoid_duplicates.py
@@ -23,19 +23,15 @@ has already received a copy, we either drop the message, add a duplicate
warning header, or pass it through, depending on the user's preferences.
"""
-from __future__ import absolute_import, print_function, unicode_literals
-
-__metaclass__ = type
__all__ = [
'AvoidDuplicates',
]
from email.utils import getaddresses, formataddr
-from zope.interface import implementer
-
from mailman.core.i18n import _
from mailman.interfaces.handler import IHandler
+from zope.interface import implementer
COMMASPACE = ', '
diff --git a/src/mailman/handlers/cleanse.py b/src/mailman/handlers/cleanse.py
index 6b653bb34..0dad3077e 100644
--- a/src/mailman/handlers/cleanse.py
+++ b/src/mailman/handlers/cleanse.py
@@ -17,9 +17,6 @@
"""Cleanse certain headers from all messages."""
-from __future__ import absolute_import, print_function, unicode_literals
-
-__metaclass__ = type
__all__ = [
'Cleanse',
]
@@ -28,11 +25,10 @@ __all__ = [
import logging
from email.utils import formataddr
-from zope.interface import implementer
-
from mailman.core.i18n import _
from mailman.handlers.cook_headers import uheader
from mailman.interfaces.handler import IHandler
+from zope.interface import implementer
log = logging.getLogger('mailman.post')
diff --git a/src/mailman/handlers/cleanse_dkim.py b/src/mailman/handlers/cleanse_dkim.py
index 225666bf1..a4c16d31e 100644
--- a/src/mailman/handlers/cleanse_dkim.py
+++ b/src/mailman/handlers/cleanse_dkim.py
@@ -25,20 +25,16 @@ and it will also give the MTA the opportunity to regenerate valid keys
originating at the Mailman server for the outgoing message.
"""
-from __future__ import absolute_import, print_function, unicode_literals
-
-__metaclass__ = type
__all__ = [
'CleanseDKIM',
]
from lazr.config import as_boolean
-from zope.interface import implementer
-
from mailman.config import config
from mailman.core.i18n import _
from mailman.interfaces.handler import IHandler
+from zope.interface import implementer
diff --git a/src/mailman/handlers/cook_headers.py b/src/mailman/handlers/cook_headers.py
index d5d096448..44ef02e36 100644
--- a/src/mailman/handlers/cook_headers.py
+++ b/src/mailman/handlers/cook_headers.py
@@ -17,9 +17,6 @@
"""Cook a message's headers."""
-from __future__ import absolute_import, print_function, unicode_literals
-
-__metaclass__ = type
__all__ = [
'CookHeaders',
]
@@ -27,21 +24,18 @@ __all__ = [
import re
-from email.errors import HeaderParseError
-from email.header import Header, decode_header, make_header
+from email.header import Header
from email.utils import parseaddr, formataddr, getaddresses
-from zope.interface import implementer
-
from mailman.core.i18n import _
from mailman.interfaces.handler import IHandler
from mailman.interfaces.mailinglist import Personalization, ReplyToMunging
from mailman.version import VERSION
+from zope.interface import implementer
COMMASPACE = ', '
MAXLINELEN = 78
-
-nonascii = re.compile('[^\s!-~]')
+NONASCII = re.compile('[^\s!-~]')
@@ -54,12 +48,12 @@ def uheader(mlist, s, header_name=None, continuation_ws='\t', maxlinelen=None):
specified.
"""
charset = mlist.preferred_language.charset
- if nonascii.search(s):
+ if NONASCII.search(s):
# use list charset but ...
if charset == 'us-ascii':
charset = 'iso-8859-1'
else:
- # there is no nonascii so ...
+ # there is no non-ascii so ...
charset = 'us-ascii'
return Header(s, charset, maxlinelen, header_name, continuation_ws)
@@ -78,13 +72,6 @@ def process(mlist, msg, msgdata):
msgdata['original_sender'] = msg.sender
# VirginRunner sets _fasttrack for internally crafted messages.
fasttrack = msgdata.get('_fasttrack')
- if not msgdata.get('isdigest') and not fasttrack:
- try:
- prefix_subject(mlist, msg, msgdata)
- except (UnicodeError, ValueError):
- # TK: Sometimes subject header is not MIME encoded for 8bit
- # simply abort prefixing.
- pass
# Add Precedence: and other useful headers. None of these are standard
# and finding information on some of them are fairly difficult. Some are
# just common practice, and we'll add more here as they become necessary.
@@ -171,114 +158,6 @@ def process(mlist, msg, msgdata):
-def prefix_subject(mlist, msg, msgdata):
- """Maybe add a subject prefix.
-
- Add the subject prefix unless the message is a digest or is being fast
- tracked (e.g. internally crafted, delivered to a single user such as the
- list admin).
- """
- if not mlist.subject_prefix.strip():
- return
- prefix = mlist.subject_prefix
- subject = msg.get('subject', '')
- # Try to figure out what the continuation_ws is for the header
- if isinstance(subject, Header):
- lines = str(subject).splitlines()
- else:
- lines = subject.splitlines()
- ws = '\t'
- if len(lines) > 1 and lines[1] and lines[1][0] in ' \t':
- ws = lines[1][0]
- msgdata['original_subject'] = subject
- # The subject may be multilingual but we take the first charset as major
- # one and try to decode. If it is decodable, returned subject is in one
- # line and cset is properly set. If fail, subject is mime-encoded and
- # cset is set as us-ascii. See detail for ch_oneline() (CookHeaders one
- # line function).
- subject, cset = ch_oneline(subject)
- # TK: Python interpreter has evolved to be strict on ascii charset code
- # range. It is safe to use unicode string when manupilating header
- # contents with re module. It would be best to return unicode in
- # ch_oneline() but here is temporary solution.
- subject = unicode(subject, cset)
- # If the subject_prefix contains '%d', it is replaced with the
- # mailing list sequential number. Sequential number format allows
- # '%d' or '%05d' like pattern.
- prefix_pattern = re.escape(prefix)
- # unescape '%' :-<
- prefix_pattern = '%'.join(prefix_pattern.split(r'\%'))
- p = re.compile('%\d*d')
- if p.search(prefix, 1):
- # prefix have number, so we should search prefix w/number in subject.
- # Also, force new style.
- prefix_pattern = p.sub(r'\s*\d+\s*', prefix_pattern)
- subject = re.sub(prefix_pattern, '', subject)
- rematch = re.match('((RE|AW|SV|VS)(\[\d+\])?:\s*)+', subject, re.I)
- if rematch:
- subject = subject[rematch.end():]
- recolon = 'Re:'
- else:
- recolon = ''
- # At this point, subject may become null if someone post mail with
- # subject: [subject prefix]
- if subject.strip() == '':
- subject = _('(no subject)')
- cset = mlist.preferred_language.charset
- # and substitute %d in prefix with post_id
- try:
- prefix = prefix % mlist.post_id
- except TypeError:
- pass
- # Get the header as a Header instance, with proper unicode conversion
- if not recolon:
- h = uheader(mlist, prefix, 'Subject', continuation_ws=ws)
- else:
- h = uheader(mlist, prefix, 'Subject', continuation_ws=ws)
- h.append(recolon)
- # TK: Subject is concatenated and unicode string.
- subject = subject.encode(cset, 'replace')
- h.append(subject, cset)
- del msg['subject']
- msg['Subject'] = h
- ss = uheader(mlist, recolon, 'Subject', continuation_ws=ws)
- ss.append(subject, cset)
- msgdata['stripped_subject'] = ss
-
-
-
-def ch_oneline(headerstr):
- # Decode header string in one line and convert into single charset.
- # Return (string, cset) tuple as check for failure.
- try:
- d = decode_header(headerstr)
- # At this point, we should rstrip() every string because some
- # MUA deliberately add trailing spaces when composing return
- # message.
- d = [(s.rstrip(), c) for (s, c) in d]
- # Find all charsets in the original header. We use 'utf-8' rather
- # than using the first charset (in mailman 2.1.x) if multiple
- # charsets are used.
- csets = []
- for (s, c) in d:
- if c and c not in csets:
- csets.append(c)
- if len(csets) == 0:
- cset = 'us-ascii'
- elif len(csets) == 1:
- cset = csets[0]
- else:
- cset = 'utf-8'
- h = make_header(d)
- ustr = unicode(h)
- oneline = ''.join(ustr.splitlines())
- return oneline.encode(cset, 'replace'), cset
- except (LookupError, UnicodeError, ValueError, HeaderParseError):
- # possibly charset problem. return with undecoded string in one line.
- return ''.join(headerstr.splitlines()), 'us-ascii'
-
-
-
@implementer(IHandler)
class CookHeaders:
"""Modify message headers."""
diff --git a/src/mailman/handlers/decorate.py b/src/mailman/handlers/decorate.py
index bf8454232..78fafb3ca 100644
--- a/src/mailman/handlers/decorate.py
+++ b/src/mailman/handlers/decorate.py
@@ -17,9 +17,6 @@
"""Decorate a message by sticking the header and footer around it."""
-from __future__ import absolute_import, print_function, unicode_literals
-
-__metaclass__ = type
__all__ = [
'Decorate',
'decorate',
@@ -31,15 +28,14 @@ import re
import logging
from email.mime.text import MIMEText
-from urllib2 import URLError
-from zope.component import getUtility
-from zope.interface import implementer
-
from mailman.core.i18n import _
from mailman.email.message import Message
from mailman.interfaces.handler import IHandler
from mailman.interfaces.templates import ITemplateLoader
from mailman.utilities.string import expand
+from six.moves.urllib_error import URLError
+from zope.component import getUtility
+from zope.interface import implementer
log = logging.getLogger('mailman.error')
diff --git a/src/mailman/handlers/docs/acknowledge.rst b/src/mailman/handlers/docs/acknowledge.rst
index e91f94f62..42cab04a0 100644
--- a/src/mailman/handlers/docs/acknowledge.rst
+++ b/src/mailman/handlers/docs/acknowledge.rst
@@ -113,9 +113,9 @@ The receipt will include the original message's subject in the response body,
1
>>> dump_msgdata(messages[0].msgdata)
_parsemsg : False
- listname : test@example.com
+ listid : test.example.com
nodecorate : True
- recipients : set([u'aperson@example.com'])
+ recipients : {'aperson@example.com'}
reduced_list_headers: True
...
>>> print(messages[0].msg.as_string())
@@ -150,9 +150,9 @@ If there is no subject, then the receipt will use a generic message.
1
>>> dump_msgdata(messages[0].msgdata)
_parsemsg : False
- listname : test@example.com
+ listid : test.example.com
nodecorate : True
- recipients : set([u'aperson@example.com'])
+ recipients : {'aperson@example.com'}
reduced_list_headers: True
...
>>> print(messages[0].msg.as_string())
diff --git a/src/mailman/handlers/docs/avoid-duplicates.rst b/src/mailman/handlers/docs/avoid-duplicates.rst
index 612634941..19a41bf85 100644
--- a/src/mailman/handlers/docs/avoid-duplicates.rst
+++ b/src/mailman/handlers/docs/avoid-duplicates.rst
@@ -71,7 +71,7 @@ or ``Resent-CC``), then they will get a list copy.
>>> msgdata = recips.copy()
>>> handler.process(mlist, msg, msgdata)
>>> sorted(msgdata['recipients'])
- [u'aperson@example.com', u'bperson@example.com']
+ ['aperson@example.com', 'bperson@example.com']
>>> print(msg.as_string())
From: Claire Person <cperson@example.com>
<BLANKLINE>
@@ -89,7 +89,7 @@ If they're mentioned on the ``CC`` line, they won't get a list copy.
>>> msgdata = recips.copy()
>>> handler.process(mlist, msg, msgdata)
>>> sorted(msgdata['recipients'])
- [u'bperson@example.com']
+ ['bperson@example.com']
>>> print(msg.as_string())
From: Claire Person <cperson@example.com>
CC: aperson@example.com
@@ -109,7 +109,7 @@ to ``True`` (the default), then they still get a list copy.
>>> msgdata = recips.copy()
>>> handler.process(mlist, msg, msgdata)
>>> sorted(msgdata['recipients'])
- [u'aperson@example.com', u'bperson@example.com']
+ ['aperson@example.com', 'bperson@example.com']
>>> print(msg.as_string())
From: Claire Person <cperson@example.com>
CC: bperson@example.com
@@ -128,7 +128,7 @@ Other headers checked for recipients include the ``To``...
>>> msgdata = recips.copy()
>>> handler.process(mlist, msg, msgdata)
>>> sorted(msgdata['recipients'])
- [u'bperson@example.com']
+ ['bperson@example.com']
>>> print(msg.as_string())
From: Claire Person <cperson@example.com>
To: aperson@example.com
@@ -147,7 +147,7 @@ Other headers checked for recipients include the ``To``...
>>> msgdata = recips.copy()
>>> handler.process(mlist, msg, msgdata)
>>> sorted(msgdata['recipients'])
- [u'bperson@example.com']
+ ['bperson@example.com']
>>> print(msg.as_string())
From: Claire Person <cperson@example.com>
Resent-To: aperson@example.com
@@ -166,7 +166,7 @@ Other headers checked for recipients include the ``To``...
>>> msgdata = recips.copy()
>>> handler.process(mlist, msg, msgdata)
>>> sorted(msgdata['recipients'])
- [u'bperson@example.com']
+ ['bperson@example.com']
>>> print(msg.as_string())
From: Claire Person <cperson@example.com>
Resent-Cc: aperson@example.com
diff --git a/src/mailman/handlers/docs/digests.rst b/src/mailman/handlers/docs/digests.rst
index ac6ea33d6..c3fc62ebf 100644
--- a/src/mailman/handlers/docs/digests.rst
+++ b/src/mailman/handlers/docs/digests.rst
@@ -82,11 +82,13 @@ actually crafted by the handler.
>>> mlist.digest_size_threshold = 1
>>> mlist.volume = 2
>>> mlist.next_digest_number = 10
+ >>> digest_path = os.path.join(mlist.data_path, 'digest.mmdf')
>>> size = 0
>>> for msg in message_factory:
... process(mlist, msg, {})
- ... size += len(str(msg))
- ... if size >= mlist.digest_size_threshold * 1024:
+ ... # When the digest reaches the proper size, it is renamed. So we
+ ... # can break out of this list when the file disappears.
+ ... if not os.path.exists(digest_path):
... break
>>> sum(1 for msg in digest_mbox(mlist))
diff --git a/src/mailman/handlers/docs/file-recips.rst b/src/mailman/handlers/docs/file-recips.rst
index 58af6f480..73b47adb1 100644
--- a/src/mailman/handlers/docs/file-recips.rst
+++ b/src/mailman/handlers/docs/file-recips.rst
@@ -34,26 +34,6 @@ returns.
recipients: 7
-Missing file
-============
-
-The include file must live inside the list's data directory, under the name
-``members.txt``. If the file doesn't exist, the list of recipients will be
-empty.
-
- >>> import os
- >>> file_path = os.path.join(mlist.data_path, 'members.txt')
- >>> open(file_path)
- Traceback (most recent call last):
- ...
- IOError: [Errno ...]
- No such file or directory: u'.../_xtest@example.com/members.txt'
- >>> msgdata = {}
- >>> handler.process(mlist, msg, msgdata)
- >>> dump_list(msgdata['recipients'])
- *Empty*
-
-
Existing file
=============
@@ -61,16 +41,15 @@ If the file exists, it contains a list of addresses, one per line. These
addresses are returned as the set of recipients.
::
- >>> fp = open(file_path, 'w')
- >>> try:
+ >>> import os
+ >>> file_path = os.path.join(mlist.data_path, 'members.txt')
+ >>> with open(file_path, 'w', encoding='utf-8') as fp:
... print('bperson@example.com', file=fp)
... print('cperson@example.com', file=fp)
... print('dperson@example.com', file=fp)
... print('eperson@example.com', file=fp)
... print('fperson@example.com', file=fp)
... print('gperson@example.com', file=fp)
- ... finally:
- ... fp.close()
>>> msgdata = {}
>>> handler.process(mlist, msg, msgdata)
diff --git a/src/mailman/handlers/docs/filtering.rst b/src/mailman/handlers/docs/filtering.rst
index 6c3735f1b..582211d54 100644
--- a/src/mailman/handlers/docs/filtering.rst
+++ b/src/mailman/handlers/docs/filtering.rst
@@ -26,6 +26,8 @@ Filtering the outer content type
A simple filtering setting will just search the content types of the messages
parts, discarding all parts with a matching MIME type. If the message's outer
content type matches the filter, the entire message will be discarded.
+However, if we turn off content filtering altogether, then the handler
+short-circuits.
::
>>> from mailman.interfaces.mime import FilterAction
@@ -42,14 +44,6 @@ content type matches the filter, the entire message will be discarded.
... """)
>>> process = config.handlers['mime-delete'].process
- >>> process(mlist, msg, {})
- Traceback (most recent call last):
- ...
- DiscardMessage: The message's content type was explicitly disallowed
-
-However, if we turn off content filtering altogether, then the handler
-short-circuits.
-
>>> mlist.filter_content = False
>>> msgdata = {}
>>> process(mlist, msg, msgdata)
@@ -74,15 +68,15 @@ crafted internally by Mailman.
MIME-Version: 1.0
<BLANKLINE>
xxxxx
- >>> msgdata
- {u'isdigest': True}
+ >>> dump_msgdata(msgdata)
+ isdigest: True
Simple multipart filtering
==========================
-If one of the subparts in a multipart message matches the filter type, then
-just that subpart will be stripped.
+If one of the subparts in a ``multipart`` message matches the filter type,
+then just that subpart will be stripped.
::
>>> msg = message_from_string("""\
@@ -241,8 +235,8 @@ name of the file containing the message payload to filter.
>>> try:
... print("""\
... import sys
- ... print 'Converted text/html to text/plain'
- ... print 'Filename:', sys.argv[1]
+ ... print('Converted text/html to text/plain')
+ ... print('Filename:', sys.argv[1])
... """, file=fp)
... finally:
... fp.close()
diff --git a/src/mailman/handlers/docs/nntp.rst b/src/mailman/handlers/docs/nntp.rst
index 2dfc95ce1..72bcb35f0 100644
--- a/src/mailman/handlers/docs/nntp.rst
+++ b/src/mailman/handlers/docs/nntp.rst
@@ -63,5 +63,5 @@ messages are gated to.
>>> dump_msgdata(messages[0].msgdata)
_parsemsg: False
- listname : test@example.com
+ listid : test.example.com
version : 3
diff --git a/src/mailman/handlers/docs/replybot.rst b/src/mailman/handlers/docs/replybot.rst
index 638c2fdc8..9e18ce911 100644
--- a/src/mailman/handlers/docs/replybot.rst
+++ b/src/mailman/handlers/docs/replybot.rst
@@ -49,9 +49,9 @@ response.
>>> dump_msgdata(messages[0].msgdata)
_parsemsg : False
- listname : _xtest@example.com
+ listid : _xtest.example.com
nodecorate : True
- recipients : set([u'aperson@example.com'])
+ recipients : {'aperson@example.com'}
reduced_list_headers: True
version : 3
@@ -141,9 +141,9 @@ Unless the ``X-Ack:`` header has a value of ``yes``, in which case, the
>>> dump_msgdata(messages[0].msgdata)
_parsemsg : False
- listname : _xtest@example.com
+ listid : _xtest.example.com
nodecorate : True
- recipients : set([u'asystem@example.com'])
+ recipients : {'asystem@example.com'}
reduced_list_headers: True
version : 3
diff --git a/src/mailman/handlers/docs/rfc-2369.rst b/src/mailman/handlers/docs/rfc-2369.rst
index 8180b0635..b5a783edc 100644
--- a/src/mailman/handlers/docs/rfc-2369.rst
+++ b/src/mailman/handlers/docs/rfc-2369.rst
@@ -13,7 +13,7 @@ headers generally start with the `List-` prefix.
..
This is a helper function for the following section.
>>> def list_headers(msg, only=None):
- ... if isinstance(only, basestring):
+ ... if isinstance(only, str):
... only = (only.lower(),)
... elif only is None:
... only = set(header.lower() for header in msg.keys()
diff --git a/src/mailman/handlers/docs/subject-munging.rst b/src/mailman/handlers/docs/subject-munging.rst
index 538ad99c7..de22a928c 100644
--- a/src/mailman/handlers/docs/subject-munging.rst
+++ b/src/mailman/handlers/docs/subject-munging.rst
@@ -1,44 +1,42 @@
-===============
-Subject munging
-===============
+================
+Subject prefixes
+================
-Messages that flow through the global pipeline get their headers *cooked*,
-which basically means that their headers go through several mostly unrelated
-transformations. Some headers get added, others get changed. Some of these
-changes depend on mailing list settings and others depend on how the message
-is getting sent through the system. We'll take things one-by-one.
+Mailing lists can define a *subject prefix* which gets added to the front of
+any ``Subject`` text. This can be used to quickly identify which mailing list
+the message was posted to.
>>> mlist = create_list('test@example.com')
+The default list style gives the mailing list a default prefix.
-Inserting a prefix
-==================
+ >>> print(mlist.subject_prefix)
+ [Test]
-Another thing header cooking does is *munge* the ``Subject`` header by
-inserting the subject prefix for the list at the front. If there's no subject
-header in the original message, Mailman uses a canned default. In order to do
-subject munging, a mailing list must have a preferred language.
-::
+This can be changed to anything, but typically ends with a trailing space.
>>> mlist.subject_prefix = '[XTest] '
- >>> mlist.preferred_language = 'en'
+ >>> process = config.handlers['subject-prefix'].process
+
+
+No Subject
+==========
+
+If the original message has no ``Subject``, then a canned one is used.
+
>>> msg = message_from_string("""\
... From: aperson@example.com
...
... A message of great import.
... """)
- >>> msgdata = {}
-
- >>> from mailman.handlers.cook_headers import process
- >>> process(mlist, msg, msgdata)
-
-The original subject header is stored in the message metadata.
-
- >>> msgdata['original_subject']
- u''
+ >>> process(mlist, msg, {})
>>> print(msg['subject'])
[XTest] (no subject)
+
+Inserting a prefix
+==================
+
If the original message had a ``Subject`` header, then the prefix is inserted
at the beginning of the header's value.
@@ -50,34 +48,12 @@ at the beginning of the header's value.
... """)
>>> msgdata = {}
>>> process(mlist, msg, msgdata)
- >>> print(msgdata['original_subject'])
- Something important
>>> print(msg['subject'])
[XTest] Something important
-``Subject`` headers are not munged for digest messages.
-
- >>> msg = message_from_string("""\
- ... From: aperson@example.com
- ... Subject: Something important
- ...
- ... A message of great import.
- ... """)
- >>> process(mlist, msg, dict(isdigest=True))
- >>> print(msg['subject'])
- Something important
-
-Nor are they munged for *fast tracked* messages, which are generally defined
-as messages that Mailman crafts internally.
+The original ``Subject`` is available in the metadata.
- >>> msg = message_from_string("""\
- ... From: aperson@example.com
- ... Subject: Something important
- ...
- ... A message of great import.
- ... """)
- >>> process(mlist, msg, dict(_fasttrack=True))
- >>> print(msg['subject'])
+ >>> print(msgdata['original_subject'])
Something important
If a ``Subject`` header already has a prefix, usually following a ``Re:``
@@ -95,8 +71,7 @@ front of the header text.
[XTest] Re: Something important
If the ``Subject`` header has a prefix at the front of the header text, that's
-where it will stay. This is called *new style* prefixing and is the only
-option available in Mailman 3.
+where it will stay.
>>> msg = message_from_string("""\
... From: aperson@example.com
@@ -122,10 +97,10 @@ set than the encoded header.
...
... """)
>>> process(mlist, msg, {})
- >>> print(msg['subject'])
+ >>> print(msg['subject'].encode())
[XTest] =?iso-2022-jp?b?GyRCJWEhPCVrJV4lcxsoQg==?=
- >>> unicode(msg['subject'])
- u'[XTest] \u30e1\u30fc\u30eb\u30de\u30f3'
+ >>> print(str(msg['subject']))
+ [XTest] メールマン
Prefix numbers
@@ -178,10 +153,10 @@ in the subject prefix, and the subject is encoded non-ASCII.
...
... """)
>>> process(mlist, msg, {})
- >>> print(msg['subject'])
+ >>> print(msg['subject'].encode())
[XTest 456] =?iso-2022-jp?b?GyRCJWEhPCVrJV4lcxsoQg==?=
- >>> unicode(msg['subject'])
- u'[XTest 456] \u30e1\u30fc\u30eb\u30de\u30f3'
+ >>> print(msg['subject'])
+ [XTest 456] メールマン
Even more fun is when the internationalized ``Subject`` header already has a
prefix, possibly with a different posting number.
@@ -191,13 +166,10 @@ prefix, possibly with a different posting number.
...
... """)
>>> process(mlist, msg, {})
- >>> print(msg['subject'])
+ >>> print(msg['subject'].encode())
[XTest 456] Re: =?iso-2022-jp?b?GyRCJWEhPCVrJV4lcxsoQg==?=
-
-..
- # XXX This requires Python email patch #1681333 to succeed.
- # >>> unicode(msg['subject'])
- # u'[XTest 456] Re: \u30e1\u30fc\u30eb\u30de\u30f3'
+ >>> print(msg['subject'])
+ [XTest 456] Re: メールマン
As before, old style subject prefixes are re-ordered.
@@ -206,14 +178,11 @@ As before, old style subject prefixes are re-ordered.
...
... """)
>>> process(mlist, msg, {})
- >>> print(msg['subject'])
+ >>> print(msg['subject'].encode())
[XTest 456] Re:
=?iso-2022-jp?b?GyRCJWEhPCVrJV4lcxsoQg==?=
-
-..
- # XXX This requires Python email patch #1681333 to succeed.
- # >>> unicode(msg['subject'])
- # u'[XTest 456] Re: \u30e1\u30fc\u30eb\u30de\u30f3'
+ >>> print(msg['subject'])
+ [XTest 456] Re: メールマン
In this test case, we get an extra space between the prefix and the original
diff --git a/src/mailman/handlers/docs/tagger.rst b/src/mailman/handlers/docs/tagger.rst
index f3303b7ef..fcefdb01c 100644
--- a/src/mailman/handlers/docs/tagger.rst
+++ b/src/mailman/handlers/docs/tagger.rst
@@ -55,7 +55,7 @@ and the message metadata gets a key with a list of matching topic names.
<BLANKLINE>
<BLANKLINE>
>>> msgdata['topichits']
- [u'bar fight']
+ ['bar fight']
Scanning body lines
@@ -114,7 +114,7 @@ found.
Keywords: barbaz
<BLANKLINE>
>>> msgdata['topichits']
- [u'bar fight']
+ ['bar fight']
However, scanning stops at the first body line that doesn't look like a
header.
@@ -161,7 +161,7 @@ When set to a negative number, all body lines will be scanned.
>>> print(msg['x-topics'])
bar fight
>>> msgdata['topichits']
- [u'bar fight']
+ ['bar fight']
Scanning sub-parts
@@ -175,14 +175,14 @@ text payload.
... Subject: Was
... Keywords: Raw
... Content-Type: multipart/alternative; boundary="BOUNDARY"
- ...
+ ...
... --BOUNDARY
... From: sabo
... To: obas
- ...
+ ...
... Subject: farbaw
... Keywords: barbaz
- ...
+ ...
... --BOUNDARY--
... """)
>>> msgdata = {}
@@ -203,7 +203,7 @@ text payload.
--BOUNDARY--
<BLANKLINE>
>>> msgdata['topichits']
- [u'bar fight']
+ ['bar fight']
But the tagger will not descend into non-text parts.
@@ -211,23 +211,23 @@ But the tagger will not descend into non-text parts.
... Subject: Was
... Keywords: Raw
... Content-Type: multipart/alternative; boundary=BOUNDARY
- ...
+ ...
... --BOUNDARY
... From: sabo
... To: obas
... Content-Type: message/rfc822
- ...
+ ...
... Subject: farbaw
... Keywords: barbaz
- ...
+ ...
... --BOUNDARY
... From: sabo
... To: obas
... Content-Type: message/rfc822
- ...
+ ...
... Subject: farbaw
... Keywords: barbaz
- ...
+ ...
... --BOUNDARY--
... """)
>>> msgdata = {}
diff --git a/src/mailman/handlers/docs/to-outgoing.rst b/src/mailman/handlers/docs/to-outgoing.rst
index e87fd4f26..90ea137a5 100644
--- a/src/mailman/handlers/docs/to-outgoing.rst
+++ b/src/mailman/handlers/docs/to-outgoing.rst
@@ -37,6 +37,6 @@ additional key set: the mailing list name.
_parsemsg: False
bar : 2
foo : 1
- listname : test@example.com
+ listid : test.example.com
verp : True
version : 3
diff --git a/src/mailman/handlers/file_recipients.py b/src/mailman/handlers/file_recipients.py
index ec8868649..4b115bb53 100644
--- a/src/mailman/handlers/file_recipients.py
+++ b/src/mailman/handlers/file_recipients.py
@@ -17,9 +17,6 @@
"""Get the normal delivery recipients from a Sendmail style :include: file."""
-from __future__ import absolute_import, print_function, unicode_literals
-
-__metaclass__ = type
__all__ = [
'FileRecipients',
]
@@ -28,10 +25,9 @@ __all__ = [
import os
import errno
-from zope.interface import implementer
-
from mailman.core.i18n import _
from mailman.interfaces.handler import IHandler
+from zope.interface import implementer
diff --git a/src/mailman/handlers/member_recipients.py b/src/mailman/handlers/member_recipients.py
index 0f99bf709..7497746eb 100644
--- a/src/mailman/handlers/member_recipients.py
+++ b/src/mailman/handlers/member_recipients.py
@@ -23,22 +23,18 @@ on the `recipients' attribute of the message. This attribute is used by the
SendmailDeliver and BulkDeliver modules.
"""
-from __future__ import absolute_import, print_function, unicode_literals
-
-__metaclass__ = type
__all__ = [
'MemberRecipients',
]
-from zope.interface import implementer
-
from mailman.config import config
from mailman.core import errors
from mailman.core.i18n import _
from mailman.interfaces.handler import IHandler
from mailman.interfaces.member import DeliveryStatus
from mailman.utilities.string import wrap
+from zope.interface import implementer
diff --git a/src/mailman/handlers/mime_delete.py b/src/mailman/handlers/mime_delete.py
index 98c1de3f9..1d107522d 100644
--- a/src/mailman/handlers/mime_delete.py
+++ b/src/mailman/handlers/mime_delete.py
@@ -24,9 +24,6 @@ wrapping only single sections after other processing are replaced by their
contents.
"""
-from __future__ import absolute_import, print_function, unicode_literals
-
-__metaclass__ = type
__all__ = [
'MIMEDelete',
]
@@ -41,9 +38,6 @@ from email.iterators import typed_subpart_iterator
from email.mime.message import MIMEMessage
from email.mime.text import MIMEText
from lazr.config import as_boolean
-from os.path import splitext
-from zope.interface import implementer
-
from mailman.config import config
from mailman.core import errors
from mailman.core.i18n import _
@@ -52,6 +46,8 @@ from mailman.interfaces.action import FilterAction
from mailman.interfaces.handler import IHandler
from mailman.utilities.string import oneline
from mailman.version import VERSION
+from os.path import splitext
+from zope.interface import implementer
log = logging.getLogger('mailman.error')
@@ -245,7 +241,7 @@ def to_plaintext(msg):
filename = tempfile.mktemp('.html')
fp = open(filename, 'w')
try:
- fp.write(subpart.get_payload(decode=True))
+ fp.write(subpart.get_payload())
fp.close()
cmd = os.popen(config.HTML_TO_PLAIN_TEXT_COMMAND %
{'filename': filename})
diff --git a/src/mailman/handlers/owner_recipients.py b/src/mailman/handlers/owner_recipients.py
index 5a1d0bd2e..dbb203728 100644
--- a/src/mailman/handlers/owner_recipients.py
+++ b/src/mailman/handlers/owner_recipients.py
@@ -17,20 +17,16 @@
"""Calculate the list owner recipients (includes moderators)."""
-from __future__ import absolute_import, print_function, unicode_literals
-
-__metaclass__ = type
__all__ = [
'OwnerRecipients',
]
-from zope.interface import implementer
-
from mailman.config import config
from mailman.core.i18n import _
from mailman.interfaces.handler import IHandler
from mailman.interfaces.member import DeliveryStatus
+from zope.interface import implementer
diff --git a/src/mailman/handlers/replybot.py b/src/mailman/handlers/replybot.py
index 63f3ca4cf..44df2344e 100644
--- a/src/mailman/handlers/replybot.py
+++ b/src/mailman/handlers/replybot.py
@@ -17,9 +17,6 @@
"""Handler for automatic responses."""
-from __future__ import absolute_import, print_function, unicode_literals
-
-__metaclass__ = type
__all__ = [
'Replybot',
]
@@ -27,9 +24,6 @@ __all__ = [
import logging
-from zope.component import getUtility
-from zope.interface import implementer
-
from mailman.core.i18n import _
from mailman.email.message import UserNotification
from mailman.interfaces.autorespond import (
@@ -38,6 +32,8 @@ from mailman.interfaces.handler import IHandler
from mailman.interfaces.usermanager import IUserManager
from mailman.utilities.datetime import today
from mailman.utilities.string import expand, wrap
+from zope.component import getUtility
+from zope.interface import implementer
log = logging.getLogger('mailman.error')
diff --git a/src/mailman/handlers/rfc_2369.py b/src/mailman/handlers/rfc_2369.py
index ea909f41b..c835f2a67 100644
--- a/src/mailman/handlers/rfc_2369.py
+++ b/src/mailman/handlers/rfc_2369.py
@@ -17,22 +17,18 @@
"""RFC 2369 List-* and related headers."""
-from __future__ import absolute_import, print_function, unicode_literals
-
-__metaclass__ = type
__all__ = [
'RFC2369',
]
from email.utils import formataddr
-from zope.interface import implementer
-
from mailman.core.i18n import _
from mailman.handlers.cook_headers import uheader
from mailman.interfaces.archiver import ArchivePolicy
from mailman.interfaces.mailinglist import IListArchiverSet
from mailman.interfaces.handler import IHandler
+from zope.interface import implementer
CONTINUATION = ',\n\t'
diff --git a/src/mailman/handlers/subject_prefix.py b/src/mailman/handlers/subject_prefix.py
new file mode 100644
index 000000000..20abd1036
--- /dev/null
+++ b/src/mailman/handlers/subject_prefix.py
@@ -0,0 +1,184 @@
+# Copyright (C) 2014 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 <http://www.gnu.org/licenses/>.
+
+"""Subject header prefix munging."""
+
+__all__ = [
+ 'SubjectPrefix',
+ ]
+
+
+import re
+
+from email.header import Header, make_header, decode_header
+from mailman.core.i18n import _
+from mailman.interfaces.handler import IHandler
+from zope.interface import implementer
+
+
+RE_PATTERN = '((RE|AW|SV|VS)(\[\d+\])?:\s*)+'
+ASCII_CHARSETS = (None, 'ascii', 'us-ascii')
+EMPTYSTRING = ''
+
+
+
+def ascii_header(mlist, msgdata, subject, prefix, prefix_pattern, ws):
+ if mlist.preferred_language.charset not in ASCII_CHARSETS:
+ return None
+ for chunk, charset in decode_header(subject.encode()):
+ if charset not in ASCII_CHARSETS:
+ return None
+ subject_text = EMPTYSTRING.join(str(subject).splitlines())
+ rematch = re.match(RE_PATTERN, subject_text, re.I)
+ if rematch:
+ subject_text = subject_text[rematch.end():]
+ recolon = 'Re: '
+ else:
+ recolon = ''
+ # At this point, the subject may become null if someone posted mail
+ # with "Subject: [subject prefix]".
+ if subject_text.strip() == '':
+ with _.using(mlist.preferred_language.code):
+ subject_text = _('(no subject)')
+ else:
+ subject_text = re.sub(prefix_pattern, '', subject_text)
+ msgdata['stripped_subject'] = subject_text
+ lines = subject_text.splitlines()
+ first_line = [lines[0]]
+ if recolon:
+ first_line.insert(0, recolon)
+ if prefix:
+ first_line.insert(0, prefix)
+ subject_text = EMPTYSTRING.join(first_line)
+ return Header(subject_text, continuation_ws=ws)
+
+
+def all_same_charset(mlist, msgdata, subject, prefix, prefix_pattern, ws):
+ list_charset = mlist.preferred_language.charset
+ chunks = []
+ for chunk, charset in decode_header(subject.encode()):
+ if charset is None:
+ charset = 'us-ascii'
+ chunks.append(chunk.decode(charset))
+ if charset != list_charset:
+ return None
+ subject_text = EMPTYSTRING.join(chunks)
+ rematch = re.match(RE_PATTERN, subject_text, re.I)
+ if rematch:
+ subject_text = subject_text[rematch.end():]
+ recolon = 'Re: '
+ else:
+ recolon = ''
+ # At this point, the subject may become null if someone posted mail
+ # with "Subject: [subject prefix]".
+ if subject_text.strip() == '':
+ with _.push(mlist.preferred_language.code):
+ subject_text = _('(no subject)')
+ else:
+ subject_text = re.sub(prefix_pattern, '', subject_text)
+ msgdata['stripped_subject'] = subject_text
+ lines = subject_text.splitlines()
+ first_line = [lines[0]]
+ if recolon:
+ first_line.insert(0, recolon)
+ if prefix:
+ first_line.insert(0, prefix)
+ subject_text = EMPTYSTRING.join(first_line)
+ return Header(subject_text, charset=list_charset, continuation_ws=ws)
+
+
+def mixed_charsets(mlist, msgdata, subject, prefix, prefix_pattern, ws):
+ list_charset = mlist.preferred_language.charset
+ chunks = decode_header(subject.encode())
+ if len(chunks) == 0:
+ with _.push(mlist.preferred_language.code):
+ subject_text = _('(no subject)')
+ chunks = [(prefix, list_charset),
+ (subject_text, list_charset),
+ ]
+ return make_header(chunks, continuation_ws=ws)
+ # Only search the first chunk for Re and existing prefix.
+ chunk_text, chunk_charset = chunks[0]
+ if chunk_charset is None:
+ chunk_charset = 'us-ascii'
+ first_text = chunk_text.decode(chunk_charset)
+ first_text = re.sub(prefix_pattern, '', first_text).lstrip()
+ rematch = re.match(RE_PATTERN, first_text, re.I)
+ if rematch:
+ first_text = 'Re: ' + first_text[rematch.end():]
+ chunks[0] = (first_text, chunk_charset)
+ # The subject text stripped of the prefix, for use in the NNTP gateway.
+ msgdata['stripped_subject'] = str(make_header(chunks, continuation_ws=ws))
+ chunks.insert(0, (prefix, list_charset))
+ return make_header(chunks, continuation_ws=ws)
+
+
+
+@implementer(IHandler)
+class SubjectPrefix:
+ """Add a list-specific prefix to the Subject header value."""
+
+ name = 'subject-prefix'
+ description = _('Add a list-specific prefix to the Subject header value.')
+
+ def process(self, mlist, msg, msgdata):
+ """See `IHandler`."""
+ if msgdata.get('isdigest') or msgdata.get('_fasttrack'):
+ return
+ prefix = mlist.subject_prefix
+ if not prefix.strip():
+ return
+ subject = msg.get('subject', '')
+ # Turn the value into a Header instance and try to figure out what
+ # continuation whitespace is being used.
+ # Save the original Subject.
+ msgdata['original_subject'] = subject
+ if isinstance(subject, Header):
+ subject_text = str(subject)
+ else:
+ subject = make_header(decode_header(subject))
+ subject_text = str(subject)
+ lines = subject_text.splitlines()
+ ws = '\t'
+ if len(lines) > 1 and lines[1] and lines[1][0] in ' \t':
+ ws = lines[1][0]
+ # If the subject_prefix contains '%d', it is replaced with the mailing
+ # list's sequence number. The sequential number format allows '%d' or
+ # '%05d' like pattern.
+ prefix_pattern = re.escape(prefix)
+ # Unescape '%'.
+ prefix_pattern = '%'.join(prefix_pattern.split(r'\%'))
+ p = re.compile('%\d*d')
+ if p.search(prefix, 1):
+ # The prefix has number, so we should search prefix w/number in
+ # subject. Also, force new style.
+ prefix_pattern = p.sub(r'\s*\d+\s*', prefix_pattern)
+ # Substitute %d in prefix with post_id
+ try:
+ prefix = prefix % mlist.post_id
+ except TypeError:
+ pass
+ for handler in (ascii_header,
+ all_same_charset,
+ mixed_charsets,
+ ):
+ new_subject = handler(
+ mlist, msgdata, subject, prefix, prefix_pattern, ws)
+ if new_subject is not None:
+ del msg['subject']
+ msg['Subject'] = new_subject
+ return
diff --git a/src/mailman/handlers/tagger.py b/src/mailman/handlers/tagger.py
index 803cc6d11..199c5907f 100644
--- a/src/mailman/handlers/tagger.py
+++ b/src/mailman/handlers/tagger.py
@@ -17,9 +17,6 @@
"""Extract topics from the original mail message."""
-from __future__ import absolute_import, print_function, unicode_literals
-
-__metaclass__ = type
__all__ = [
'Tagger',
]
@@ -29,15 +26,14 @@ import re
import email.iterators
import email.parser
-from zope.interface import implementer
-
from mailman.core.i18n import _
from mailman.interfaces.handler import IHandler
+from zope.interface import implementer
OR = '|'
CRNL = '\r\n'
-EMPTYBYTES = b''
+EMPTYSTRING = ''
NLTAB = '\n\t'
@@ -104,7 +100,7 @@ def scanbody(msg, numlines=None):
reader = list(email.iterators.body_line_iterator(msg))
while numlines is None or lineno < numlines:
try:
- line = bytes(reader.pop(0))
+ line = reader.pop(0)
except IndexError:
break
# Blank lines don't count
@@ -115,7 +111,7 @@ def scanbody(msg, numlines=None):
# Concatenate those body text lines with newlines, and then create a new
# message object from those lines.
p = _ForgivingParser()
- msg = p.parsestr(EMPTYBYTES.join(lines))
+ msg = p.parsestr(EMPTYSTRING.join(lines))
return msg.get_all('subject', []) + msg.get_all('keywords', [])
diff --git a/src/mailman/handlers/tests/test_cook_headers.py b/src/mailman/handlers/tests/test_cook_headers.py
index d83a44f20..385f402c5 100644
--- a/src/mailman/handlers/tests/test_cook_headers.py
+++ b/src/mailman/handlers/tests/test_cook_headers.py
@@ -17,9 +17,6 @@
"""Test the cook_headers handler."""
-from __future__ import absolute_import, print_function, unicode_literals
-
-__metaclass__ = type
__all__ = [
'TestCookHeaders',
]
@@ -50,6 +47,6 @@ class TestCookHeaders(unittest.TestCase):
for msg in messages:
try:
cook_headers.process(self._mlist, msg, {})
- except AttributeError as e:
+ except AttributeError as error:
# LP: #1130696 would raise an AttributeError on .sender
- self.fail(e)
+ self.fail(error)
diff --git a/src/mailman/handlers/tests/test_file_recips.py b/src/mailman/handlers/tests/test_file_recips.py
new file mode 100644
index 000000000..906530762
--- /dev/null
+++ b/src/mailman/handlers/tests/test_file_recips.py
@@ -0,0 +1,73 @@
+# Copyright (C) 2014 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 <http://www.gnu.org/licenses/>.
+
+"""Test file-recips handler."""
+
+__all__ = [
+ 'TestFileRecips',
+ ]
+
+
+import os
+import unittest
+
+from mailman.app.lifecycle import create_list
+from mailman.config import config
+from mailman.testing.helpers import specialized_message_from_string as mfs
+from mailman.testing.layers import ConfigLayer
+
+
+
+class TestFileRecips(unittest.TestCase):
+ layer = ConfigLayer
+
+ def setUp(self):
+ self._mlist = create_list('test@example.com')
+ self._handler = config.handlers['file-recipients'].process
+ self._msg = mfs("""\
+From: aperson@example.com
+
+A message.
+""")
+
+ def test_file_is_missing(self):
+ # It is not an error for the list's the members.txt file to be
+ # missing. The missing file is just ignored.
+ msgdata = {}
+ self._handler(self._mlist, self._msg, msgdata)
+ self.assertEqual(msgdata['recipients'], set())
+
+ def test_file_exists(self):
+ # Like above, but the file exists and contains recipients.
+ path = os.path.join(self._mlist.data_path, 'members.txt')
+ with open(path, 'w', encoding='utf-8') as fp:
+ print('bperson@example.com', file=fp)
+ print('cperson@example.com', file=fp)
+ print('dperson@example.com', file=fp)
+ print('eperson@example.com', file=fp)
+ print('fperson@example.com', file=fp)
+ print('gperson@example.com', file=fp)
+ msgdata = {}
+ self._handler(self._mlist, self._msg, msgdata)
+ self.assertEqual(msgdata['recipients'], set((
+ 'bperson@example.com',
+ 'cperson@example.com',
+ 'dperson@example.com',
+ 'eperson@example.com',
+ 'fperson@example.com',
+ 'gperson@example.com',
+ )))
diff --git a/src/mailman/handlers/tests/test_filter.py b/src/mailman/handlers/tests/test_filter.py
new file mode 100644
index 000000000..b81744008
--- /dev/null
+++ b/src/mailman/handlers/tests/test_filter.py
@@ -0,0 +1,57 @@
+# Copyright (C) 2014 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 <http://www.gnu.org/licenses/>.
+
+"""Test the filter handler."""
+
+__all__ = [
+ 'TestFilters',
+ ]
+
+
+import unittest
+
+from mailman.app.lifecycle import create_list
+from mailman.config import config
+from mailman.core.errors import DiscardMessage
+from mailman.interfaces.mime import FilterAction
+from mailman.testing.helpers import specialized_message_from_string as mfs
+from mailman.testing.layers import ConfigLayer
+
+
+
+class TestFilters(unittest.TestCase):
+ layer = ConfigLayer
+
+ def setUp(self):
+ self._mlist = create_list('test@example.com')
+
+ def test_discard_when_outer_type_matches(self):
+ # When the outer MIME type of the message matches a filter type, the
+ # entire message is discarded.
+ self._mlist.filter_content = True
+ self._mlist.filter_types = ['image/jpeg']
+ self._mlist.filter_action = FilterAction.discard
+ msg = mfs("""\
+From: aperson@example.com
+Content-Type: image/jpeg
+MIME-Version: 1.0
+
+xxxxx
+""")
+ self.assertRaises(DiscardMessage,
+ config.handlers['mime-delete'].process,
+ self._mlist, msg, {})
diff --git a/src/mailman/handlers/tests/test_mimedel.py b/src/mailman/handlers/tests/test_mimedel.py
index c7c37152f..02cb275e0 100644
--- a/src/mailman/handlers/tests/test_mimedel.py
+++ b/src/mailman/handlers/tests/test_mimedel.py
@@ -17,9 +17,6 @@
"""Test the mime_delete handler."""
-from __future__ import absolute_import, print_function, unicode_literals
-
-__metaclass__ = type
__all__ = [
'TestDispose',
]
@@ -27,8 +24,6 @@ __all__ = [
import unittest
-from zope.component import getUtility
-
from mailman.app.lifecycle import create_list
from mailman.config import config
from mailman.core import errors
@@ -40,6 +35,7 @@ from mailman.testing.helpers import (
LogFileMark, configuration, get_queue_messages,
specialized_message_from_string as mfs)
from mailman.testing.layers import ConfigLayer
+from zope.component import getUtility
diff --git a/src/mailman/handlers/tests/test_recipients.py b/src/mailman/handlers/tests/test_recipients.py
index afe533a7e..688dcce04 100644
--- a/src/mailman/handlers/tests/test_recipients.py
+++ b/src/mailman/handlers/tests/test_recipients.py
@@ -17,9 +17,6 @@
"""Testing various recipients stuff."""
-from __future__ import absolute_import, print_function, unicode_literals
-
-__metaclass__ = type
__all__ = [
'TestMemberRecipients',
'TestOwnerRecipients',
@@ -28,13 +25,14 @@ __all__ = [
import unittest
-from zope.component import getUtility
from mailman.app.lifecycle import create_list
from mailman.config import config
from mailman.interfaces.member import DeliveryMode, DeliveryStatus, MemberRole
from mailman.interfaces.usermanager import IUserManager
-from mailman.testing.helpers import specialized_message_from_string as mfs
+from mailman.testing.helpers import (
+ configuration, specialized_message_from_string as mfs)
from mailman.testing.layers import ConfigLayer
+from zope.component import getUtility
@@ -199,23 +197,14 @@ To: test-owner@example.com
self._process(self._mlist, self._msg, msgdata)
self.assertEqual(msgdata['recipients'], set(('noreply@example.com',)))
- def test_site_admin_unicode(self):
- # Since the config file is read as bytes, the site_owner is also a
- # bytes and must be converted to unicode when used as a fallback.
+ @configuration('mailman', site_owner='siteadmin@example.com')
+ def test_no_owners_site_owner_fallback(self):
+ # The list has no owners or moderators, but there is a non-default
+ # site owner defined. That owner gets the message.
self._cris.unsubscribe()
self._dave.unsubscribe()
self.assertEqual(self._mlist.administrators.member_count, 0)
msgdata = {}
- # In order to properly mimic the testing environment, use
- # config.push()/config.pop() directly instead of using the
- # configuration() context manager.
- config.push('test_site_admin_unicode', b"""\
-[mailman]
-site_owner: siteadmin@example.com
-""")
- try:
- self._process(self._mlist, self._msg, msgdata)
- finally:
- config.pop('test_site_admin_unicode')
- self.assertEqual(len(msgdata['recipients']), 1)
- self.assertIsInstance(list(msgdata['recipients'])[0], unicode)
+ self._process(self._mlist, self._msg, msgdata)
+ self.assertEqual(msgdata['recipients'],
+ set(('siteadmin@example.com',)))
diff --git a/src/mailman/handlers/tests/test_subject_prefix.py b/src/mailman/handlers/tests/test_subject_prefix.py
new file mode 100644
index 000000000..f4fd8c113
--- /dev/null
+++ b/src/mailman/handlers/tests/test_subject_prefix.py
@@ -0,0 +1,129 @@
+# Copyright (C) 2014 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 <http://www.gnu.org/licenses/>.
+
+"""Test the Subject header prefix munging.."""
+
+__all__ = [
+ 'TestSubjectPrefix',
+ ]
+
+
+import unittest
+
+from mailman.app.lifecycle import create_list
+from mailman.config import config
+from mailman.email.message import Message
+from mailman.testing.layers import ConfigLayer
+
+
+
+class TestSubjectPrefix(unittest.TestCase):
+ layer = ConfigLayer
+
+ def setUp(self):
+ self._mlist = create_list('test@example.com')
+ self._process = config.handlers['subject-prefix'].process
+
+ def test_isdigest(self):
+ # If the message is destined for the digest, the Subject header does
+ # not get touched.
+ msg = Message()
+ msg['Subject'] = 'A test message'
+ self._process(self._mlist, msg, dict(isdigest=True))
+ self.assertEqual(str(msg['subject']), 'A test message')
+
+ def test_fasttrack(self):
+ # Messages internally crafted are 'fast tracked' and don't get their
+ # Subjects prefixed either.
+ msg = Message()
+ msg['Subject'] = 'A test message'
+ self._process(self._mlist, msg, dict(_fasttrack=True))
+ self.assertEqual(str(msg['subject']), 'A test message')
+
+ def test_whitespace_only_prefix(self):
+ # If the Subject prefix only contains whitespace, ignore it.
+ self._mlist.subject_prefix = ' '
+ msg = Message()
+ msg['Subject'] = 'A test message'
+ self._process(self._mlist, msg, dict(_fasttrack=True))
+ self.assertEqual(str(msg['subject']), 'A test message')
+
+ def test_save_original_subject(self):
+ # When the Subject gets prefixed, the original is saved in the message
+ # metadata.
+ msgdata = {}
+ msg = Message()
+ msg['Subject'] = 'A test message'
+ self._process(self._mlist, msg, msgdata)
+ self.assertEqual(msgdata['original_subject'], 'A test message')
+
+ def test_prefix(self):
+ # The Subject gets prefixed. The prefix gets automatically set by the
+ # list style when the list gets created.
+ msg = Message()
+ msg['Subject'] = 'A test message'
+ self._process(self._mlist, msg, {})
+ self.assertEqual(str(msg['subject']), '[Test] A test message')
+
+ def test_no_double_prefix(self):
+ # Don't add a prefix if the subject already contains one.
+ msg = Message()
+ msg['Subject'] = '[Test] A test message'
+ self._process(self._mlist, msg, {})
+ self.assertEqual(str(msg['subject']), '[Test] A test message')
+
+ def test_re_prefix(self):
+ # The subject has a Re: prefix. Make sure that gets preserved, but
+ # after the list prefix.
+ msg = Message()
+ msg['Subject'] = 'Re: [Test] A test message'
+ self._process(self._mlist, msg, {})
+ self.assertEqual(str(msg['subject']), '[Test] Re: A test message')
+
+ def test_multiline_subject(self):
+ # The subject appears on multiple lines.
+ msg = Message()
+ msg['Subject'] = '\n A test message'
+ self._process(self._mlist, msg, {})
+ self.assertEqual(str(msg['subject']), '[Test] A test message')
+
+ def test_i18n_prefix(self):
+ # The Subject header is encoded, but the prefix is still added.
+ msg = Message()
+ msg['Subject'] = '=?iso-2022-jp?b?GyRCJWEhPCVrJV4lcxsoQg==?='
+ self._process(self._mlist, msg, {})
+ subject = msg['subject']
+ self.assertEqual(subject.encode(),
+ '[Test] =?iso-2022-jp?b?GyRCJWEhPCVrJV4lcxsoQg==?=')
+ self.assertEqual(str(subject), '[Test] メールマン')
+
+ def test_i18n_subject_with_sequential_prefix_and_re(self):
+ # The mailing list defines a sequential prefix, and the original
+ # Subject has a prefix with a different sequence number, *and* it also
+ # contains a Re: prefix. Make sure the sequence gets updated and all
+ # the bits get put back together in the right order.
+ self._mlist.subject_prefix = '[Test %d]'
+ self._mlist.post_id = 456
+ msg = Message()
+ msg['Subject'] = \
+ '[Test 123] Re: =?iso-2022-jp?b?GyRCJWEhPCVrJV4lcxsoQg==?='
+ self._process(self._mlist, msg, {})
+ subject = msg['subject']
+ self.assertEqual(
+ subject.encode(),
+ '[Test 456] Re: =?iso-2022-jp?b?GyRCJWEhPCVrJV4lcxsoQg==?=')
+ self.assertEqual(str(subject), '[Test 456] Re: メールマン')
diff --git a/src/mailman/handlers/tests/test_to_digest.py b/src/mailman/handlers/tests/test_to_digest.py
index 451ebf9a5..8562c3fd7 100644
--- a/src/mailman/handlers/tests/test_to_digest.py
+++ b/src/mailman/handlers/tests/test_to_digest.py
@@ -17,9 +17,6 @@
"""Test the to_digest handler."""
-from __future__ import absolute_import, print_function, unicode_literals
-
-__metaclass__ = type
__all__ = [
'TestToDigest',
]
diff --git a/src/mailman/handlers/to_archive.py b/src/mailman/handlers/to_archive.py
index d18742f3c..d8c61bc7d 100644
--- a/src/mailman/handlers/to_archive.py
+++ b/src/mailman/handlers/to_archive.py
@@ -17,20 +17,16 @@
"""Add the message to the archives."""
-from __future__ import absolute_import, print_function, unicode_literals
-
-__metaclass__ = type
__all__ = [
'ToArchive',
]
-from zope.interface import implementer
-
from mailman.config import config
from mailman.core.i18n import _
from mailman.interfaces.archiver import ArchivePolicy
from mailman.interfaces.handler import IHandler
+from zope.interface import implementer
diff --git a/src/mailman/handlers/to_digest.py b/src/mailman/handlers/to_digest.py
index e915bbfa3..70aeb0dcc 100644
--- a/src/mailman/handlers/to_digest.py
+++ b/src/mailman/handlers/to_digest.py
@@ -17,9 +17,6 @@
"""Add the message to the list's current digest."""
-from __future__ import absolute_import, print_function, unicode_literals
-
-__metaclass__ = type
__all__ = [
'ToDigest',
]
@@ -27,8 +24,6 @@ __all__ = [
import os
-from zope.interface import implementer
-
from mailman.config import config
from mailman.core.i18n import _
from mailman.email.message import Message
@@ -36,6 +31,7 @@ from mailman.interfaces.digests import DigestFrequency
from mailman.interfaces.handler import IHandler
from mailman.utilities.datetime import now as right_now
from mailman.utilities.mailbox import Mailbox
+from zope.interface import implementer
@@ -55,7 +51,7 @@ class ToDigest:
mailbox_path = os.path.join(mlist.data_path, 'digest.mmdf')
# Lock the mailbox and append the message.
with Mailbox(mailbox_path, create=True) as mbox:
- mbox.add(msg.as_string())
+ mbox.add(msg)
# Calculate the current size of the mailbox file. This will not tell
# us exactly how big the resulting MIME and rfc1153 digest will
# actually be, but it's the most easily available metric to decide
@@ -75,7 +71,7 @@ class ToDigest:
os.rename(mailbox_path, mailbox_dest)
config.switchboards['digest'].enqueue(
Message(),
- listname=mlist.fqdn_listname,
+ listid=mlist.list_id,
digest_path=mailbox_dest,
volume=volume,
digest_number=digest_number)
diff --git a/src/mailman/handlers/to_outgoing.py b/src/mailman/handlers/to_outgoing.py
index 6dfbe88c0..95686d9c7 100644
--- a/src/mailman/handlers/to_outgoing.py
+++ b/src/mailman/handlers/to_outgoing.py
@@ -22,19 +22,15 @@ posted to the list membership. Anything else that needs to go out to some
recipient should just be placed in the out queue directly.
"""
-from __future__ import absolute_import, print_function, unicode_literals
-
-__metaclass__ = type
__all__ = [
'ToOutgoing',
]
-from zope.interface import implementer
-
from mailman.config import config
from mailman.core.i18n import _
from mailman.interfaces.handler import IHandler
+from zope.interface import implementer
@@ -47,5 +43,4 @@ class ToOutgoing:
def process(self, mlist, msg, msgdata):
"""See `IHandler`."""
- config.switchboards['out'].enqueue(
- msg, msgdata, listname=mlist.fqdn_listname)
+ config.switchboards['out'].enqueue(msg, msgdata, listid=mlist.list_id)
diff --git a/src/mailman/handlers/to_usenet.py b/src/mailman/handlers/to_usenet.py
index d5a946644..8d86ea86e 100644
--- a/src/mailman/handlers/to_usenet.py
+++ b/src/mailman/handlers/to_usenet.py
@@ -17,9 +17,6 @@
"""Move the message to the mail->news queue."""
-from __future__ import absolute_import, print_function, unicode_literals
-
-__metaclass__ = type
__all__ = [
'ToUsenet',
]
@@ -27,14 +24,13 @@ __all__ = [
import logging
-from zope.interface import implementer
-
from mailman.config import config
from mailman.core.i18n import _
from mailman.interfaces.handler import IHandler
+from zope.interface import implementer
-COMMASPACE = ', '
+COMMASPACE = ', '
log = logging.getLogger('mailman.error')
@@ -65,5 +61,4 @@ class ToUsenet:
COMMASPACE.join(error))
return
# Put the message in the news runner's queue.
- config.switchboards['nntp'].enqueue(
- msg, msgdata, listname=mlist.fqdn_listname)
+ config.switchboards['nntp'].enqueue(msg, msgdata, listid=mlist.list_id)
diff --git a/src/mailman/interfaces/action.py b/src/mailman/interfaces/action.py
index 5d4b150a3..c4147f57a 100644
--- a/src/mailman/interfaces/action.py
+++ b/src/mailman/interfaces/action.py
@@ -17,7 +17,6 @@
"""Message actions."""
-__metaclass__ = type
__all__ = [
'Action',
'FilterAction',
diff --git a/src/mailman/interfaces/address.py b/src/mailman/interfaces/address.py
index 28a9e8ef4..24d0899f5 100644
--- a/src/mailman/interfaces/address.py
+++ b/src/mailman/interfaces/address.py
@@ -17,9 +17,6 @@
"""Interface for email address related information."""
-from __future__ import absolute_import, print_function, unicode_literals
-
-__metaclass__ = type
__all__ = [
'AddressAlreadyLinkedError',
'AddressError',
@@ -33,9 +30,8 @@ __all__ = [
]
-from zope.interface import Interface, Attribute
-
from mailman.interfaces.errors import MailmanError
+from zope.interface import Interface, Attribute
diff --git a/src/mailman/interfaces/archiver.py b/src/mailman/interfaces/archiver.py
index 8b843bc60..b2fc4f1af 100644
--- a/src/mailman/interfaces/archiver.py
+++ b/src/mailman/interfaces/archiver.py
@@ -17,9 +17,6 @@
"""Interface for archiving schemes."""
-from __future__ import absolute_import, print_function, unicode_literals
-
-__metaclass__ = type
__all__ = [
'ArchivePolicy',
'ClobberDate',
diff --git a/src/mailman/interfaces/autorespond.py b/src/mailman/interfaces/autorespond.py
index 8da2fc795..d53e181f0 100644
--- a/src/mailman/interfaces/autorespond.py
+++ b/src/mailman/interfaces/autorespond.py
@@ -17,9 +17,6 @@
"""Autoresponder."""
-from __future__ import absolute_import, print_function, unicode_literals
-
-__metaclass__ = type
__all__ = [
'ALWAYS_REPLY',
'IAutoResponseRecord',
@@ -33,6 +30,7 @@ from datetime import timedelta
from enum import Enum
from zope.interface import Interface, Attribute
+
ALWAYS_REPLY = timedelta()
diff --git a/src/mailman/interfaces/bans.py b/src/mailman/interfaces/bans.py
index 48b3415c8..ea19abc38 100644
--- a/src/mailman/interfaces/bans.py
+++ b/src/mailman/interfaces/bans.py
@@ -17,9 +17,6 @@
"""Manager of email address bans."""
-from __future__ import absolute_import, print_function, unicode_literals
-
-__metaclass__ = type
__all__ = [
'IBan',
'IBanManager',
diff --git a/src/mailman/interfaces/bounce.py b/src/mailman/interfaces/bounce.py
index 8a0ffd4b2..9556830eb 100644
--- a/src/mailman/interfaces/bounce.py
+++ b/src/mailman/interfaces/bounce.py
@@ -17,9 +17,6 @@
"""Interface to bounce detection components."""
-from __future__ import absolute_import, print_function, unicode_literals
-
-__metaclass__ = type
__all__ = [
'BounceContext',
'IBounceEvent',
diff --git a/src/mailman/interfaces/chain.py b/src/mailman/interfaces/chain.py
index 85bad22a4..788112f0b 100644
--- a/src/mailman/interfaces/chain.py
+++ b/src/mailman/interfaces/chain.py
@@ -17,9 +17,6 @@
"""Interfaces describing the basics of chains and links."""
-from __future__ import absolute_import, print_function, unicode_literals
-
-__metaclass__ = type
__all__ = [
'AcceptEvent',
'AcceptOwnerEvent',
diff --git a/src/mailman/interfaces/command.py b/src/mailman/interfaces/command.py
index 720e59ee8..a73d0b1de 100644
--- a/src/mailman/interfaces/command.py
+++ b/src/mailman/interfaces/command.py
@@ -17,9 +17,6 @@
"""Interfaces defining email commands."""
-from __future__ import absolute_import, print_function, unicode_literals
-
-__metaclass__ = type
__all__ = [
'ContinueProcessing',
'ICLISubCommand',
diff --git a/src/mailman/interfaces/configuration.py b/src/mailman/interfaces/configuration.py
index 65547d44d..49d0bb3c6 100644
--- a/src/mailman/interfaces/configuration.py
+++ b/src/mailman/interfaces/configuration.py
@@ -17,9 +17,6 @@
"""Configuration system interface."""
-from __future__ import absolute_import, print_function, unicode_literals
-
-__metaclass__ = type
__all__ = [
'ConfigurationUpdatedEvent',
'IConfiguration',
@@ -27,9 +24,8 @@ __all__ = [
]
-from zope.interface import Interface
-
from mailman.core.errors import MailmanError
+from zope.interface import Interface
diff --git a/src/mailman/interfaces/database.py b/src/mailman/interfaces/database.py
index 9ca05b747..37830329a 100644
--- a/src/mailman/interfaces/database.py
+++ b/src/mailman/interfaces/database.py
@@ -17,9 +17,6 @@
"""Interfaces for database interaction."""
-from __future__ import absolute_import, print_function, unicode_literals
-
-__metaclass__ = type
__all__ = [
'DatabaseError',
'IDatabase',
diff --git a/src/mailman/interfaces/digests.py b/src/mailman/interfaces/digests.py
index c5231e488..c343669f5 100644
--- a/src/mailman/interfaces/digests.py
+++ b/src/mailman/interfaces/digests.py
@@ -17,9 +17,6 @@
"""One last digest."""
-from __future__ import absolute_import, print_function, unicode_literals
-
-__metaclass__ = type
__all__ = [
'IOneLastDigest'
]
diff --git a/src/mailman/interfaces/domain.py b/src/mailman/interfaces/domain.py
index a4f929ddb..aed76ebe9 100644
--- a/src/mailman/interfaces/domain.py
+++ b/src/mailman/interfaces/domain.py
@@ -17,9 +17,6 @@
"""Interface representing domains."""
-from __future__ import absolute_import, print_function, unicode_literals
-
-__metaclass__ = type
__all__ = [
'BadDomainSpecificationError',
'DomainCreatedEvent',
diff --git a/src/mailman/interfaces/errors.py b/src/mailman/interfaces/errors.py
index 187c329b3..ecb4270f1 100644
--- a/src/mailman/interfaces/errors.py
+++ b/src/mailman/interfaces/errors.py
@@ -22,9 +22,6 @@ components. More specific exceptions will be located in the relevant
interfaces.
"""
-from __future__ import absolute_import, print_function, unicode_literals
-
-__metaclass__ = type
__all__ = [
'MailmanError',
]
diff --git a/src/mailman/interfaces/handler.py b/src/mailman/interfaces/handler.py
index 2e6c3fa20..6c52f017b 100644
--- a/src/mailman/interfaces/handler.py
+++ b/src/mailman/interfaces/handler.py
@@ -17,9 +17,6 @@
"""Interface describing a pipeline handler."""
-from __future__ import absolute_import, print_function, unicode_literals
-
-__metaclass__ = type
__all__ = [
'IHandler',
]
diff --git a/src/mailman/interfaces/languages.py b/src/mailman/interfaces/languages.py
index 9e88dd78f..810de7af1 100644
--- a/src/mailman/interfaces/languages.py
+++ b/src/mailman/interfaces/languages.py
@@ -17,9 +17,6 @@
"""Interfaces for managing languages."""
-from __future__ import absolute_import, print_function, unicode_literals
-
-__metaclass__ = type
__all__ = [
'ILanguage',
'ILanguageManager',
diff --git a/src/mailman/interfaces/listmanager.py b/src/mailman/interfaces/listmanager.py
index 7fe8ed35a..27b6b5838 100644
--- a/src/mailman/interfaces/listmanager.py
+++ b/src/mailman/interfaces/listmanager.py
@@ -17,9 +17,6 @@
"""Interface for list storage, deleting, and finding."""
-from __future__ import absolute_import, print_function, unicode_literals
-
-__metaclass__ = type
__all__ = [
'IListManager',
'ListAlreadyExistsError',
diff --git a/src/mailman/interfaces/mailinglist.py b/src/mailman/interfaces/mailinglist.py
index 3900e3349..2d145dc6c 100644
--- a/src/mailman/interfaces/mailinglist.py
+++ b/src/mailman/interfaces/mailinglist.py
@@ -17,9 +17,6 @@
"""Interface for a mailing list."""
-from __future__ import absolute_import, print_function, unicode_literals
-
-__metaclass__ = type
__all__ = [
'IAcceptableAlias',
'IAcceptableAliasSet',
@@ -32,9 +29,8 @@ __all__ = [
from enum import Enum
-from zope.interface import Interface, Attribute
-
from mailman.interfaces.member import MemberRole
+from zope.interface import Interface, Attribute
diff --git a/src/mailman/interfaces/member.py b/src/mailman/interfaces/member.py
index e2a5dc4fe..9e3917b86 100644
--- a/src/mailman/interfaces/member.py
+++ b/src/mailman/interfaces/member.py
@@ -17,9 +17,6 @@
"""Interface describing the basics of a member."""
-from __future__ import absolute_import, print_function, unicode_literals
-
-__metaclass__ = type
__all__ = [
'AlreadySubscribedError',
'DeliveryMode',
@@ -37,9 +34,8 @@ __all__ = [
from enum import Enum
-from zope.interface import Interface, Attribute
-
from mailman.core.errors import MailmanError
+from zope.interface import Interface, Attribute
diff --git a/src/mailman/interfaces/messages.py b/src/mailman/interfaces/messages.py
index 7b99578c4..c78971dfd 100644
--- a/src/mailman/interfaces/messages.py
+++ b/src/mailman/interfaces/messages.py
@@ -17,9 +17,6 @@
"""The message storage service."""
-from __future__ import absolute_import, print_function, unicode_literals
-
-__metaclass__ = type
__all__ = [
'IMessage',
'IMessageStore',
diff --git a/src/mailman/interfaces/mime.py b/src/mailman/interfaces/mime.py
index 4729c426c..11feca331 100644
--- a/src/mailman/interfaces/mime.py
+++ b/src/mailman/interfaces/mime.py
@@ -17,9 +17,6 @@
"""MIME content filtering."""
-from __future__ import absolute_import, print_function, unicode_literals
-
-__metaclass__ = type
__all__ = [
'FilterAction',
'FilterType',
diff --git a/src/mailman/interfaces/mlistrequest.py b/src/mailman/interfaces/mlistrequest.py
index 77451f8bf..2af0f1776 100644
--- a/src/mailman/interfaces/mlistrequest.py
+++ b/src/mailman/interfaces/mlistrequest.py
@@ -17,9 +17,6 @@
"""Interface for a web request accessing a mailing list."""
-from __future__ import absolute_import, print_function, unicode_literals
-
-__metaclass__ = type
__all__ = [
'IMailingListRequest',
]
diff --git a/src/mailman/interfaces/mta.py b/src/mailman/interfaces/mta.py
index 22c3d121e..44c0aba42 100644
--- a/src/mailman/interfaces/mta.py
+++ b/src/mailman/interfaces/mta.py
@@ -17,9 +17,6 @@
"""Interface for mail transport agent integration."""
-from __future__ import absolute_import, print_function, unicode_literals
-
-__metaclass__ = type
__all__ = [
'IMailTransportAgentAliases',
'IMailTransportAgentDelivery',
@@ -27,9 +24,8 @@ __all__ = [
]
-from zope.interface import Interface
-
from mailman.core.errors import MailmanError
+from zope.interface import Interface
diff --git a/src/mailman/interfaces/nntp.py b/src/mailman/interfaces/nntp.py
index 8e73c2c50..46d705489 100644
--- a/src/mailman/interfaces/nntp.py
+++ b/src/mailman/interfaces/nntp.py
@@ -17,9 +17,6 @@
"""NNTP and newsgroup interfaces."""
-from __future__ import absolute_import, print_function, unicode_literals
-
-__metaclass__ = type
__all__ = [
'NewsgroupModeration',
]
diff --git a/src/mailman/interfaces/pending.py b/src/mailman/interfaces/pending.py
index a97552306..ff156d95a 100644
--- a/src/mailman/interfaces/pending.py
+++ b/src/mailman/interfaces/pending.py
@@ -22,9 +22,6 @@ maps these events to a unique hash that can be used as a token for end user
confirmation.
"""
-from __future__ import absolute_import, print_function, unicode_literals
-
-__metaclass__ = type
__all__ = [
'IPendable',
'IPended',
diff --git a/src/mailman/interfaces/permissions.py b/src/mailman/interfaces/permissions.py
index 8d06e9ffb..cf32936ff 100644
--- a/src/mailman/interfaces/permissions.py
+++ b/src/mailman/interfaces/permissions.py
@@ -17,9 +17,6 @@
"""Interfaces for various permissions."""
-from __future__ import absolute_import, print_function, unicode_literals
-
-__metaclass__ = type
__all__ = [
'IPostingPermission',
]
diff --git a/src/mailman/interfaces/pipeline.py b/src/mailman/interfaces/pipeline.py
index 817ebfc62..4ce11d8a6 100644
--- a/src/mailman/interfaces/pipeline.py
+++ b/src/mailman/interfaces/pipeline.py
@@ -17,9 +17,6 @@
"""Interface for describing pipelines."""
-from __future__ import absolute_import, print_function, unicode_literals
-
-__metaclass__ = type
__all__ = [
'IPipeline',
]
@@ -37,4 +34,3 @@ class IPipeline(Interface):
def __iter__():
"""Iterate over all the handlers in this pipeline."""
-
diff --git a/src/mailman/interfaces/preferences.py b/src/mailman/interfaces/preferences.py
index 27ae49faa..b68d7a0f5 100644
--- a/src/mailman/interfaces/preferences.py
+++ b/src/mailman/interfaces/preferences.py
@@ -17,9 +17,6 @@
"""Interface for preferences."""
-from __future__ import absolute_import, print_function, unicode_literals
-
-__metaclass__ = type
__all__ = [
'IPreferences',
]
diff --git a/src/mailman/interfaces/registrar.py b/src/mailman/interfaces/registrar.py
index 413f3284e..df7c4ed86 100644
--- a/src/mailman/interfaces/registrar.py
+++ b/src/mailman/interfaces/registrar.py
@@ -22,9 +22,6 @@ etc. than the IUserManager. The latter does no validation, syntax checking,
or confirmation, while this interface does.
"""
-from __future__ import absolute_import, print_function, unicode_literals
-
-__metaclass__ = type
__all__ = [
'ConfirmationNeededEvent',
'IRegistrar',
diff --git a/src/mailman/interfaces/requests.py b/src/mailman/interfaces/requests.py
index 4dcb3cace..ed3540e4c 100644
--- a/src/mailman/interfaces/requests.py
+++ b/src/mailman/interfaces/requests.py
@@ -21,9 +21,6 @@ The request database handles events that must be approved by the list
moderators, such as subscription requests and held messages.
"""
-from __future__ import absolute_import, print_function, unicode_literals
-
-__metaclass__ = type
__all__ = [
'IListRequests',
'RequestType',
diff --git a/src/mailman/interfaces/roster.py b/src/mailman/interfaces/roster.py
index c4a7f5567..79c9fd573 100644
--- a/src/mailman/interfaces/roster.py
+++ b/src/mailman/interfaces/roster.py
@@ -17,9 +17,6 @@
"""Interface for a roster of members."""
-from __future__ import absolute_import, print_function, unicode_literals
-
-__metaclass__ = type
__all__ = [
'IRoster',
]
diff --git a/src/mailman/interfaces/rules.py b/src/mailman/interfaces/rules.py
index feb773fca..2118a0b43 100644
--- a/src/mailman/interfaces/rules.py
+++ b/src/mailman/interfaces/rules.py
@@ -17,9 +17,6 @@
"""Interface describing the basics of rules."""
-from __future__ import absolute_import, print_function, unicode_literals
-
-__metaclass__ = type
__all__ = [
'IRule',
]
diff --git a/src/mailman/interfaces/runner.py b/src/mailman/interfaces/runner.py
index 9cb554597..74038ab71 100644
--- a/src/mailman/interfaces/runner.py
+++ b/src/mailman/interfaces/runner.py
@@ -17,9 +17,6 @@
"""Interface for runners."""
-from __future__ import absolute_import, print_function, unicode_literals
-
-__metaclass__ = type
__all__ = [
'IRunner',
'RunnerCrashEvent',
diff --git a/src/mailman/interfaces/styles.py b/src/mailman/interfaces/styles.py
index 33ab8ee84..615cb6abd 100644
--- a/src/mailman/interfaces/styles.py
+++ b/src/mailman/interfaces/styles.py
@@ -17,9 +17,6 @@
"""Interfaces for list styles."""
-from __future__ import absolute_import, print_function, unicode_literals
-
-__metaclass__ = type
__all__ = [
'DuplicateStyleError',
'IStyle',
@@ -27,8 +24,8 @@ __all__ = [
]
-from zope.interface import Interface, Attribute
from mailman.interfaces.errors import MailmanError
+from zope.interface import Interface, Attribute
diff --git a/src/mailman/interfaces/subscriptions.py b/src/mailman/interfaces/subscriptions.py
index 64d4280d6..036cc4631 100644
--- a/src/mailman/interfaces/subscriptions.py
+++ b/src/mailman/interfaces/subscriptions.py
@@ -17,18 +17,14 @@
"""Membership interface for REST."""
-from __future__ import absolute_import, print_function, unicode_literals
-
-__metaclass__ = type
__all__ = [
'ISubscriptionService',
]
-from zope.interface import Interface
-
from mailman.interfaces.errors import MailmanError
from mailman.interfaces.member import DeliveryMode, MemberRole
+from zope.interface import Interface
diff --git a/src/mailman/interfaces/switchboard.py b/src/mailman/interfaces/switchboard.py
index ae613700a..c763c142b 100644
--- a/src/mailman/interfaces/switchboard.py
+++ b/src/mailman/interfaces/switchboard.py
@@ -17,9 +17,6 @@
"""Interface for switchboards."""
-from __future__ import absolute_import, print_function, unicode_literals
-
-__metaclass__ = type
__all__ = [
'ISwitchboard',
]
diff --git a/src/mailman/interfaces/system.py b/src/mailman/interfaces/system.py
index 83992629c..36aa3279e 100644
--- a/src/mailman/interfaces/system.py
+++ b/src/mailman/interfaces/system.py
@@ -17,9 +17,6 @@
"""System information."""
-from __future__ import absolute_import, print_function, unicode_literals
-
-__metaclass__ = type
__all__ = [
'ISystem',
]
diff --git a/src/mailman/interfaces/templates.py b/src/mailman/interfaces/templates.py
index de5fa11a9..9e39747a3 100644
--- a/src/mailman/interfaces/templates.py
+++ b/src/mailman/interfaces/templates.py
@@ -17,9 +17,6 @@
"""Template downloader with cache."""
-from __future__ import absolute_import, print_function, unicode_literals
-
-__metaclass__ = type
__all__ = [
'ITemplateLoader',
]
diff --git a/src/mailman/interfaces/user.py b/src/mailman/interfaces/user.py
index e1c1df243..c42bb6c33 100644
--- a/src/mailman/interfaces/user.py
+++ b/src/mailman/interfaces/user.py
@@ -17,9 +17,6 @@
"""Interface describing the basics of a user."""
-from __future__ import absolute_import, print_function, unicode_literals
-
-__metaclass__ = type
__all__ = [
'IUser',
'PasswordChangeEvent',
@@ -27,9 +24,8 @@ __all__ = [
]
-from zope.interface import Interface, Attribute
-
from mailman.interfaces.address import AddressError
+from zope.interface import Interface, Attribute
diff --git a/src/mailman/interfaces/usermanager.py b/src/mailman/interfaces/usermanager.py
index f37d39f6a..ab58347dc 100644
--- a/src/mailman/interfaces/usermanager.py
+++ b/src/mailman/interfaces/usermanager.py
@@ -17,9 +17,6 @@
"""Interface describing the user management service."""
-from __future__ import absolute_import, print_function, unicode_literals
-
-__metaclass__ = type
__all__ = [
'IUserManager',
]
diff --git a/src/mailman/languages/language.py b/src/mailman/languages/language.py
index 35e142559..de406e10c 100644
--- a/src/mailman/languages/language.py
+++ b/src/mailman/languages/language.py
@@ -18,17 +18,13 @@
"""The representation of a language."""
-from __future__ import absolute_import, print_function, unicode_literals
-
-__metaclass__ = type
__all__ = [
'Language',
]
-from zope.interface import implementer
-
from mailman.interfaces.languages import ILanguage
+from zope.interface import implementer
diff --git a/src/mailman/languages/manager.py b/src/mailman/languages/manager.py
index 7e73c11b0..2732d490a 100644
--- a/src/mailman/languages/manager.py
+++ b/src/mailman/languages/manager.py
@@ -17,20 +17,16 @@
"""Language manager."""
-from __future__ import absolute_import, print_function, unicode_literals
-
-__metaclass__ = type
__all__ = [
'LanguageManager',
]
-from zope.component import getUtility
-from zope.interface import implementer
-
from mailman.interfaces.configuration import ConfigurationUpdatedEvent
from mailman.interfaces.languages import ILanguageManager
from mailman.languages.language import Language
+from zope.component import getUtility
+from zope.interface import implementer
diff --git a/src/mailman/model/address.py b/src/mailman/model/address.py
index 5d1994567..5ded77dd8 100644
--- a/src/mailman/model/address.py
+++ b/src/mailman/model/address.py
@@ -17,26 +17,22 @@
"""Model for addresses."""
-from __future__ import absolute_import, print_function, unicode_literals
-
-__metaclass__ = type
__all__ = [
'Address',
]
from email.utils import formataddr
+from mailman.database.model import Model
+from mailman.interfaces.address import (
+ AddressVerificationEvent, IAddress, IEmailValidator)
+from mailman.utilities.datetime import now
from sqlalchemy import Column, DateTime, ForeignKey, Integer, Unicode
from sqlalchemy.orm import relationship, backref
from zope.component import getUtility
from zope.event import notify
from zope.interface import implementer
-from mailman.database.model import Model
-from mailman.interfaces.address import (
- AddressVerificationEvent, IAddress, IEmailValidator)
-from mailman.utilities.datetime import now
-
@implementer(IAddress)
diff --git a/src/mailman/model/autorespond.py b/src/mailman/model/autorespond.py
index cfb9e017d..332d04521 100644
--- a/src/mailman/model/autorespond.py
+++ b/src/mailman/model/autorespond.py
@@ -17,25 +17,21 @@
"""Module stuff."""
-from __future__ import absolute_import, print_function, unicode_literals
-
-__metaclass__ = type
__all__ = [
'AutoResponseRecord',
'AutoResponseSet',
]
-from sqlalchemy import Column, Date, ForeignKey, Integer, desc
-from sqlalchemy.orm import relationship
-from zope.interface import implementer
-
from mailman.database.model import Model
from mailman.database.transaction import dbconnection
from mailman.database.types import Enum
from mailman.interfaces.autorespond import (
IAutoResponseRecord, IAutoResponseSet, Response)
from mailman.utilities.datetime import today
+from sqlalchemy import Column, Date, ForeignKey, Integer, desc
+from sqlalchemy.orm import relationship
+from zope.interface import implementer
diff --git a/src/mailman/model/bans.py b/src/mailman/model/bans.py
index 8678fc1e7..3ad11cbf6 100644
--- a/src/mailman/model/bans.py
+++ b/src/mailman/model/bans.py
@@ -17,9 +17,6 @@
"""Ban manager."""
-from __future__ import absolute_import, print_function, unicode_literals
-
-__metaclass__ = type
__all__ = [
'BanManager',
]
@@ -27,12 +24,11 @@ __all__ = [
import re
-from sqlalchemy import Column, Integer, Unicode
-from zope.interface import implementer
-
from mailman.database.model import Model
from mailman.database.transaction import dbconnection
from mailman.interfaces.bans import IBan, IBanManager
+from sqlalchemy import Column, Integer, Unicode
+from zope.interface import implementer
diff --git a/src/mailman/model/bounce.py b/src/mailman/model/bounce.py
index 26ebbe0c6..585a92594 100644
--- a/src/mailman/model/bounce.py
+++ b/src/mailman/model/bounce.py
@@ -17,9 +17,6 @@
"""Bounce support."""
-from __future__ import absolute_import, print_function, unicode_literals
-
-__metaclass__ = type
__all__ = [
'BounceEvent',
'BounceProcessor',
@@ -27,15 +24,14 @@ __all__ = [
-from sqlalchemy import Boolean, Column, DateTime, Integer, Unicode
-from zope.interface import implementer
-
from mailman.database.model import Model
from mailman.database.transaction import dbconnection
from mailman.database.types import Enum
from mailman.interfaces.bounce import (
BounceContext, IBounceEvent, IBounceProcessor)
from mailman.utilities.datetime import now
+from sqlalchemy import Boolean, Column, DateTime, Integer, Unicode
+from zope.interface import implementer
diff --git a/src/mailman/model/digests.py b/src/mailman/model/digests.py
index 7bfd512b6..8e8f7dedd 100644
--- a/src/mailman/model/digests.py
+++ b/src/mailman/model/digests.py
@@ -17,22 +17,18 @@
"""One last digest."""
-from __future__ import absolute_import, print_function, unicode_literals
-
-__metaclass__ = type
__all__ = [
'OneLastDigest',
]
-from sqlalchemy import Column, Integer, ForeignKey
-from sqlalchemy.orm import relationship
-from zope.interface import implementer
-
from mailman.database.model import Model
from mailman.database.types import Enum
from mailman.interfaces.digests import IOneLastDigest
from mailman.interfaces.member import DeliveryMode
+from sqlalchemy import Column, Integer, ForeignKey
+from sqlalchemy.orm import relationship
+from zope.interface import implementer
diff --git a/src/mailman/model/docs/addresses.rst b/src/mailman/model/docs/addresses.rst
index 795afe43c..9f34efdec 100644
--- a/src/mailman/model/docs/addresses.rst
+++ b/src/mailman/model/docs/addresses.rst
@@ -205,23 +205,9 @@ case-preserved version are available on attributes of the `IAddress` object.
FPERSON@example.com
Because addresses are case-insensitive for all other purposes, you cannot
-create an address that differs only in case.
-
- >>> user_manager.create_address('fperson@example.com')
- Traceback (most recent call last):
- ...
- ExistingAddressError: FPERSON@example.com
- >>> user_manager.create_address('fperson@EXAMPLE.COM')
- Traceback (most recent call last):
- ...
- ExistingAddressError: FPERSON@example.com
- >>> user_manager.create_address('FPERSON@example.com')
- Traceback (most recent call last):
- ...
- ExistingAddressError: FPERSON@example.com
-
-You can get the address using either the lower cased version or case-preserved
-version. In fact, searching for an address is case insensitive.
+create an address that differs only in case. You can get the address using
+either the lower cased version or case-preserved version. In fact, searching
+for an address is case insensitive.
>>> print(user_manager.get_address('fperson@example.com').email)
fperson@example.com
diff --git a/src/mailman/model/docs/domains.rst b/src/mailman/model/docs/domains.rst
index 153f6c19d..abb594a62 100644
--- a/src/mailman/model/docs/domains.rst
+++ b/src/mailman/model/docs/domains.rst
@@ -108,12 +108,7 @@ In the global domain manager, domains are indexed by their email host name.
base_url: http://lists.example.net,
contact_address: postmaster@example.com>
- >>> print(manager['doesnotexist.com'])
- Traceback (most recent call last):
- ...
- KeyError: u'doesnotexist.com'
-
-As with a dictionary, you can also get the domain. If the domain does not
+As with dictionaries, you can also get the domain. If the domain does not
exist, ``None`` or a default is returned.
::
@@ -128,13 +123,6 @@ exist, ``None`` or a default is returned.
>>> print(manager.get('doesnotexist.com', 'blahdeblah'))
blahdeblah
-Non-existent domains cannot be removed.
-
- >>> manager.remove('doesnotexist.com')
- Traceback (most recent call last):
- ...
- KeyError: u'doesnotexist.com'
-
Confirmation tokens
===================
diff --git a/src/mailman/model/docs/languages.rst b/src/mailman/model/docs/languages.rst
index fedea0e6e..4ed1f8ef2 100644
--- a/src/mailman/model/docs/languages.rst
+++ b/src/mailman/model/docs/languages.rst
@@ -62,7 +62,7 @@ You can iterate over all the known language codes.
>>> mgr.add('pl', 'iso-8859-2', 'Polish')
<Language [pl] Polish>
>>> sorted(mgr.codes)
- [u'en', u'it', u'pl']
+ ['en', 'it', 'pl']
You can iterate over all the known languages.
@@ -89,7 +89,7 @@ You can get a particular language by its code.
>>> print(mgr['xx'].code)
Traceback (most recent call last):
...
- KeyError: u'xx'
+ KeyError: 'xx'
>>> print(mgr.get('it').description)
Italian
>>> print(mgr.get('xx'))
diff --git a/src/mailman/model/docs/listmanager.rst b/src/mailman/model/docs/listmanager.rst
index 151bee1fe..8ff6ad3b0 100644
--- a/src/mailman/model/docs/listmanager.rst
+++ b/src/mailman/model/docs/listmanager.rst
@@ -34,22 +34,6 @@ the mailing list to the system.
>>> print(mlist.list_id)
test.example.com
-If you try to create a mailing list with the same name as an existing list,
-you will get an exception.
-
- >>> list_manager.create('test@example.com')
- Traceback (most recent call last):
- ...
- ListAlreadyExistsError: test@example.com
-
-It is an error to create a mailing list that isn't a fully qualified list name
-(i.e. posting address).
-
- >>> list_manager.create('foo')
- Traceback (most recent call last):
- ...
- InvalidEmailAddressError: foo
-
Deleting a mailing list
=======================
diff --git a/src/mailman/model/docs/mailinglist.rst b/src/mailman/model/docs/mailinglist.rst
index 3d01710c5..00a01662b 100644
--- a/src/mailman/model/docs/mailinglist.rst
+++ b/src/mailman/model/docs/mailinglist.rst
@@ -114,7 +114,8 @@ Subscribing users
An alternative way of subscribing to a mailing list is as a user with a
preferred address. This way the user can change their subscription address
just by changing their preferred address.
-::
+
+The user must have a preferred address.
>>> from mailman.utilities.datetime import now
>>> user = user_manager.create_user('dperson@example.com', 'Dave Person')
@@ -122,6 +123,8 @@ just by changing their preferred address.
>>> address.verified_on = now()
>>> user.preferred_address = address
+The preferred address is used in the subscription.
+
>>> mlist.subscribe(user)
<Member: Dave Person <dperson@example.com> on aardvark@example.com
as MemberRole.member>
@@ -132,6 +135,10 @@ just by changing their preferred address.
<Member: Dave Person <dperson@example.com> on aardvark@example.com
as MemberRole.member>
+If the user's preferred address changes, their subscribed email address also
+changes automatically.
+::
+
>>> new_address = user.register('dave.person@example.com')
>>> new_address.verified_on = now()
>>> user.preferred_address = new_address
@@ -143,31 +150,12 @@ just by changing their preferred address.
<Member: dave.person@example.com on aardvark@example.com
as MemberRole.member>
-A user is not allowed to subscribe more than once to the mailing list.
-
- >>> mlist.subscribe(user)
- Traceback (most recent call last):
- ...
- AlreadySubscribedError: <User "Dave Person" (1) at ...>
- is already a MemberRole.member of mailing list aardvark@example.com
-
-However, they are allowed to subscribe again with a specific address, even if
-this address is their preferred address.
+A user is allowed to explicitly subscribe again with a specific address, even
+if this address is their preferred address.
>>> mlist.subscribe(user.preferred_address)
<Member: dave.person@example.com
on aardvark@example.com as MemberRole.member>
-A user cannot subscribe to a mailing list without a preferred address.
-
- >>> user = user_manager.create_user('eperson@example.com', 'Elly Person')
- >>> address = list(user.addresses)[0]
- >>> address.verified_on = now()
- >>> mlist.subscribe(user)
- Traceback (most recent call last):
- ...
- MissingPreferredAddressError: User must have a preferred address:
- <User "Elly Person" (2) at ...>
-
.. _`RFC 2369`: http://www.faqs.org/rfcs/rfc2369.html
diff --git a/src/mailman/model/docs/membership.rst b/src/mailman/model/docs/membership.rst
index 2a6c99fc0..60ccd1ac1 100644
--- a/src/mailman/model/docs/membership.rst
+++ b/src/mailman/model/docs/membership.rst
@@ -2,10 +2,10 @@
List memberships
================
-Users represent people in Mailman. Users control email addresses, and rosters
-are collections of members. A member gives an email address a role, such as
-`member`, `administrator`, or `moderator`. Even nonmembers are represented by
-a roster.
+Users represent people in Mailman, members represent subscriptions. Users
+control email addresses, and rosters are collections of members. A member
+ties a subscribed email address to a role, such as `member`, `administrator`,
+or `moderator`. Even non-members are represented by a roster.
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.
@@ -228,18 +228,6 @@ regardless of their role.
fperson@example.com MemberRole.nonmember
-Double subscriptions
-====================
-
-It is an error to subscribe someone to a list with the same role twice.
-
- >>> mlist.subscribe(address_1, MemberRole.owner)
- Traceback (most recent call last):
- ...
- AlreadySubscribedError: aperson@example.com is already a MemberRole.owner
- of mailing list ant@example.com
-
-
Moderation actions
==================
@@ -276,7 +264,7 @@ Changing subscriptions
When a user is subscribed to a mailing list via a specific address they
control (as opposed to being subscribed with their preferred address), they
can change their delivery address by setting the appropriate parameter. Note
-though that the address their changing to must be verified.
+though that the address they're changing to must be verified.
>>> bee = create_list('bee@example.com')
>>> gwen = user_manager.create_user('gwen@example.com')
@@ -290,20 +278,6 @@ Gwen gets a email address.
>>> new_address = gwen.register('gperson@example.com')
-She wants to change her membership in the `test` mailing list to use her new
-address, but the address is not yet verified.
-
- >>> gwen_member.address = new_address
- Traceback (most recent call last):
- ...
- UnverifiedAddressError: gperson@example.com
-
-Her membership has not changed.
-
- >>> for m in bee.members.members:
- ... print(m.member_id.int, m.mailing_list.list_id, m.address.email)
- 7 bee.example.com gwen@example.com
-
Gwen verifies her email address, and updates her membership.
>>> from mailman.utilities.datetime import now
diff --git a/src/mailman/model/docs/messagestore.rst b/src/mailman/model/docs/messagestore.rst
index f2f2ca9d2..933ca5619 100644
--- a/src/mailman/model/docs/messagestore.rst
+++ b/src/mailman/model/docs/messagestore.rst
@@ -6,28 +6,20 @@ 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``.
+``X-Message-ID-Hash`` is the base-32 SHA1 hash of the ``Message-ID``.
>>> from mailman.interfaces.messages import IMessageStore
>>> from zope.component import getUtility
>>> message_store = getUtility(IMessageStore)
-If you try to add a message to the store which is missing the ``Message-ID``
-header, you will get an exception.
+A message with a ``Message-ID`` header can be stored.
>>> msg = message_from_string("""\
... Subject: An important message
+ ... Message-ID: <87myycy5eh.fsf@uwakimon.sk.tsukuba.ac.jp>
...
... This message is very important.
... """)
- >>> message_store.add(msg)
- Traceback (most recent call last):
- ...
- ValueError: Exactly one Message-ID header required
-
-However, if the message has a ``Message-ID`` header, it can be stored.
-
- >>> msg['Message-ID'] = '<87myycy5eh.fsf@uwakimon.sk.tsukuba.ac.jp>'
>>> x_message_id_hash = message_store.add(msg)
>>> print(x_message_id_hash)
AGDWSNXXKCWEILKKNYTBOHRDQGOX3Y35
@@ -97,15 +89,7 @@ Deleting messages from the store
================================
You delete a message from the storage service by providing the ``Message-ID``
-for the message you want to delete. If you try to delete a ``Message-ID``
-that isn't in the store, you get an exception.
-
- >>> message_store.delete_message('nothing')
- Traceback (most recent call last):
- ...
- LookupError: nothing
-
-But if you delete an existing message, it really gets deleted.
+for the message you want to delete.
>>> message_id = message['message-id']
>>> message_store.delete_message(message_id)
diff --git a/src/mailman/model/docs/pending.rst b/src/mailman/model/docs/pending.rst
index d8206b264..a634322a1 100644
--- a/src/mailman/model/docs/pending.rst
+++ b/src/mailman/model/docs/pending.rst
@@ -33,12 +33,12 @@ token that can be used in urls and such.
>>> 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 dictionary) from
-the database that matches the token. If the token isn't in the database, None
-is returned.
+There's not much you can do with tokens except to *confirm* them, which
+basically means returning the `IPendable` structure (as a dictionary) from the
+database that matches the token. If the token isn't in the database, None is
+returned.
- >>> pendable = pendingdb.confirm(bytes('missing'))
+ >>> pendable = pendingdb.confirm(b'missing')
>>> print(pendable)
None
>>> pendable = pendingdb.confirm(token)
diff --git a/src/mailman/model/docs/registration.rst b/src/mailman/model/docs/registration.rst
index 32ee27316..55e99f23a 100644
--- a/src/mailman/model/docs/registration.rst
+++ b/src/mailman/model/docs/registration.rst
@@ -8,7 +8,7 @@ additional information they may supply. All registered email addresses must
be verified before Mailman will send them any list traffic.
The ``IUserManager`` manages users, but it does so at a fairly low level.
-Specifically, it does not handle verifications, email address syntax validity
+Specifically, it does not handle verification, email address syntax validity
checks, etc. The ``IRegistrar`` is the interface to the object handling all
this stuff.
@@ -19,7 +19,7 @@ this stuff.
Here is a helper function to check the token strings.
>>> def check_token(token):
- ... assert isinstance(token, basestring), 'Not a string'
+ ... assert isinstance(token, str), 'Not a string'
... assert len(token) == 40, 'Unexpected length: %d' % len(token)
... assert token.isalnum(), 'Not alphanumeric'
... print('ok')
@@ -47,31 +47,6 @@ Some amount of sanity checks are performed on the email address, although
honestly, not as much as probably should be done. Still, some patently bad
addresses are rejected outright.
- >>> registrar.register(mlist, '')
- Traceback (most recent call last):
- ...
- InvalidEmailAddressError
- >>> registrar.register(mlist, 'some name@example.com')
- Traceback (most recent call last):
- ...
- InvalidEmailAddressError: some name@example.com
- >>> registrar.register(mlist, '<script>@example.com')
- Traceback (most recent call last):
- ...
- InvalidEmailAddressError: <script>@example.com
- >>> registrar.register(mlist, '\xa0@example.com')
- Traceback (most recent call last):
- ...
- InvalidEmailAddressError: \xa0@example.com
- >>> registrar.register(mlist, 'noatsign')
- Traceback (most recent call last):
- ...
- InvalidEmailAddressError: noatsign
- >>> registrar.register(mlist, 'nodom@ain')
- Traceback (most recent call last):
- ...
- InvalidEmailAddressError: nodom@ain
-
Register an email address
=========================
@@ -149,9 +124,9 @@ message is sent to the user in order to verify the registered address.
<BLANKLINE>
>>> dump_msgdata(items[0].msgdata)
_parsemsg : False
- listname : alpha@example.com
+ listid : alpha.example.com
nodecorate : True
- recipients : set([u'aperson@example.com'])
+ recipients : {'aperson@example.com'}
reduced_list_headers: True
version : 3
@@ -312,7 +287,7 @@ Corner cases
If you try to confirm a token that doesn't exist in the pending database, the
confirm method will just return False.
- >>> registrar.confirm(bytes('no token'))
+ >>> registrar.confirm(bytes(b'no token'))
False
Likewise, if you try to confirm, through the `IUserRegistrar` interface, a
diff --git a/src/mailman/model/docs/usermanager.rst b/src/mailman/model/docs/usermanager.rst
index 9a8c35c00..ba328b54b 100644
--- a/src/mailman/model/docs/usermanager.rst
+++ b/src/mailman/model/docs/usermanager.rst
@@ -44,7 +44,7 @@ A user can be assigned a real name.
A user can be assigned a password.
- >>> user.password = b'secret'
+ >>> user.password = 'secret'
>>> dump_list(user.password for user in user_manager.users)
secret
diff --git a/src/mailman/model/docs/users.rst b/src/mailman/model/docs/users.rst
index 2e7333944..0b926d6a7 100644
--- a/src/mailman/model/docs/users.rst
+++ b/src/mailman/model/docs/users.rst
@@ -20,7 +20,7 @@ User data
Users may have a real name and a password.
>>> user_1 = user_manager.create_user()
- >>> user_1.password = b'my password'
+ >>> user_1.password = 'my password'
>>> user_1.display_name = 'Zoe Person'
>>> dump_list(user.display_name for user in user_manager.users)
Zoe Person
@@ -30,7 +30,7 @@ Users may have a real name and a password.
The password and real name can be changed at any time.
>>> user_1.display_name = 'Zoe X. Person'
- >>> user_1.password = b'another password'
+ >>> user_1.password = 'another password'
>>> dump_list(user.display_name for user in user_manager.users)
Zoe X. Person
>>> dump_list(user.password for user in user_manager.users)
@@ -44,7 +44,7 @@ When the user's password is changed, an event is triggered.
... saved_event = event
>>> from mailman.testing.helpers import event_subscribers
>>> with event_subscribers(save_event):
- ... user_1.password = b'changed again'
+ ... user_1.password = 'changed again'
>>> print(saved_event)
<PasswordChangeEvent Zoe X. Person>
@@ -59,20 +59,13 @@ The event holds a reference to the `IUser` that changed their password.
Basic user identification
=========================
-Although rarely visible to users, every user has a unique ID in Mailman, which
-never changes. This ID is generated randomly at the time the user is created,
-and is represented by a UUID.
+Although rarely visible to users, every user has a unique immutable ID. This
+ID is generated randomly at the time the user is created, and is represented
+by a UUID.
>>> print(user_1.user_id)
00000000-0000-0000-0000-000000000001
-The user id cannot change.
-
- >>> user_1.user_id = 'foo'
- Traceback (most recent call last):
- ...
- AttributeError: can't set attribute
-
User records also have a date on which they where created.
# The test suite uses a predictable timestamp.
@@ -84,8 +77,8 @@ Users addresses
===============
One of the pieces of information that a user links to is a set of email
-addresses they control, in the form of IAddress objects. A user can control
-many addresses, but addresses may be controlled by only one user.
+addresses they control, in the form of ``IAddress`` objects. A user can
+control many addresses, but addresses may be linked to only one user.
The easiest way to link a user to an address is to just register the new
address on a user object.
@@ -114,14 +107,6 @@ You can also create the address separately and then link it to the user.
<BLANKLINE>
Zoe Person
-But don't try to link an address to more than one user.
-
- >>> another_user = user_manager.create_user()
- >>> another_user.link(address_1)
- Traceback (most recent call last):
- ...
- AddressAlreadyLinkedError: zperson@example.net
-
You can also ask whether a given user controls a given address.
>>> user_1.controls(address_1.email)
@@ -149,17 +134,6 @@ Addresses can also be unlinked from a user.
>>> print(user_manager.get_user('aperson@example.net'))
None
-But don't try to unlink the address from a user it's not linked to.
-
- >>> user_1.unlink(address_1)
- Traceback (most recent call last):
- ...
- AddressNotLinkedError: zperson@example.net
- >>> another_user.unlink(address_1)
- Traceback (most recent call last):
- ...
- AddressNotLinkedError: zperson@example.net
-
Preferred address
=================
@@ -183,20 +157,10 @@ preferred address.
>>> print(user_2.preferred_address)
None
-The preferred address must be explicitly registered, however only verified
-address may be registered as preferred.
-
- >>> anne
- <Address: Anne Person <anne@example.com> [not verified] at ...>
- >>> user_2.preferred_address = anne
- Traceback (most recent call last):
- ...
- UnverifiedAddressError: Anne Person <anne@example.com>
-
-Once the address has been verified though, it can be set as the preferred
-address, but only if the address is either controlled by the user or
-uncontrolled. In the latter case, setting it as the preferred address makes
-it controlled by the user.
+Once the address has been verified, it can be set as the preferred address,
+but only if the address is either controlled by the user or uncontrolled. In
+the latter case, setting it as the preferred address makes it controlled by
+the user.
::
>>> from mailman.utilities.datetime import now
@@ -217,17 +181,6 @@ it controlled by the user.
>>> user_2.controls(aperson.email)
True
- >>> zperson = user_manager.get_address('zperson@example.com')
- >>> zperson.verified_on = now()
- >>> user_2.controls(zperson.email)
- False
- >>> user_1.controls(zperson.email)
- True
- >>> user_2.preferred_address = zperson
- Traceback (most recent call last):
- ...
- AddressAlreadyLinkedError: Zoe Person <zperson@example.com>
-
A user can disavow their preferred address.
>>> user_2.preferred_address
@@ -328,11 +281,11 @@ membership role.
>>> from zope.interface.verify import verifyObject
>>> verifyObject(IRoster, memberships)
True
- >>> members = sorted(memberships.members)
- >>> len(members)
- 4
>>> def sortkey(member):
... return member.address.email, member.mailing_list, member.role.value
+ >>> members = sorted(memberships.members, key=sortkey)
+ >>> len(members)
+ 4
>>> for member in sorted(members, key=sortkey):
... print(member.address.email, member.mailing_list.list_id,
... member.role)
diff --git a/src/mailman/model/domain.py b/src/mailman/model/domain.py
index 8290cb755..b9d2c88ab 100644
--- a/src/mailman/model/domain.py
+++ b/src/mailman/model/domain.py
@@ -17,26 +17,22 @@
"""Domains."""
-from __future__ import absolute_import, print_function, unicode_literals
-
-__metaclass__ = type
__all__ = [
'Domain',
'DomainManager',
]
-from sqlalchemy import Column, Integer, Unicode
-from urlparse import urljoin, urlparse
-from zope.event import notify
-from zope.interface import implementer
-
from mailman.database.model import Model
from mailman.database.transaction import dbconnection
from mailman.interfaces.domain import (
BadDomainSpecificationError, DomainCreatedEvent, DomainCreatingEvent,
DomainDeletedEvent, DomainDeletingEvent, IDomain, IDomainManager)
from mailman.model.mailinglist import MailingList
+from six.moves.urllib_parse import urljoin, urlparse
+from sqlalchemy import Column, Integer, Unicode
+from zope.event import notify
+from zope.interface import implementer
diff --git a/src/mailman/model/language.py b/src/mailman/model/language.py
index f4d48fc97..7317b6328 100644
--- a/src/mailman/model/language.py
+++ b/src/mailman/model/language.py
@@ -17,19 +17,15 @@
"""Model for languages."""
-from __future__ import absolute_import, print_function, unicode_literals
-
-__metaclass__ = type
__all__ = [
'Language',
]
-from sqlalchemy import Column, Integer, Unicode
-from zope.interface import implementer
-
from mailman.database.model import Model
from mailman.interfaces.languages import ILanguage
+from sqlalchemy import Column, Integer, Unicode
+from zope.interface import implementer
diff --git a/src/mailman/model/listmanager.py b/src/mailman/model/listmanager.py
index 261490a92..7c228bcb9 100644
--- a/src/mailman/model/listmanager.py
+++ b/src/mailman/model/listmanager.py
@@ -17,17 +17,11 @@
"""A mailing list manager."""
-from __future__ import absolute_import, print_function, unicode_literals
-
-__metaclass__ = type
__all__ = [
'ListManager',
]
-from zope.event import notify
-from zope.interface import implementer
-
from mailman.database.transaction import dbconnection
from mailman.interfaces.address import InvalidEmailAddressError
from mailman.interfaces.listmanager import (
@@ -36,6 +30,8 @@ from mailman.interfaces.listmanager import (
from mailman.model.mailinglist import MailingList
from mailman.model.mime import ContentFilter
from mailman.utilities.datetime import now
+from zope.event import notify
+from zope.interface import implementer
diff --git a/src/mailman/model/mailinglist.py b/src/mailman/model/mailinglist.py
index 761a78b94..ea3317bb6 100644
--- a/src/mailman/model/mailinglist.py
+++ b/src/mailman/model/mailinglist.py
@@ -17,9 +17,6 @@
"""Model for mailing lists."""
-from __future__ import absolute_import, print_function, unicode_literals
-
-__metaclass__ = type
__all__ = [
'MailingList',
]
@@ -27,16 +24,6 @@ __all__ = [
import os
-from sqlalchemy import (
- Boolean, Column, DateTime, Float, ForeignKey, Integer, Interval,
- LargeBinary, PickleType, Unicode)
-from sqlalchemy.event import listen
-from sqlalchemy.orm import relationship
-from urlparse import urljoin
-from zope.component import getUtility
-from zope.event import notify
-from zope.interface import implementer
-
from mailman.config import config
from mailman.database.model import Model
from mailman.database.transaction import dbconnection
@@ -65,6 +52,15 @@ from mailman.model.mime import ContentFilter
from mailman.model.preferences import Preferences
from mailman.utilities.filesystem import makedirs
from mailman.utilities.string import expand
+from six.moves.urllib_parse import urljoin
+from sqlalchemy import (
+ Boolean, Column, DateTime, Float, ForeignKey, Integer, Interval,
+ LargeBinary, PickleType, Unicode)
+from sqlalchemy.event import listen
+from sqlalchemy.orm import relationship
+from zope.component import getUtility
+from zope.event import notify
+from zope.interface import implementer
SPACE = ' '
@@ -482,7 +478,9 @@ class MailingList(Model):
Member._user == subscriber).first()
if member:
raise AlreadySubscribedError(
- self.fqdn_listname, subscriber, role)
+ self.fqdn_listname,
+ subscriber.preferred_address.email,
+ role)
else:
raise ValueError('subscriber must be an address or user')
member = Member(role=role,
diff --git a/src/mailman/model/member.py b/src/mailman/model/member.py
index 9da9d5d0d..19a30074e 100644
--- a/src/mailman/model/member.py
+++ b/src/mailman/model/member.py
@@ -17,18 +17,10 @@
"""Model for members."""
-from __future__ import absolute_import, print_function, unicode_literals
-
-__metaclass__ = type
__all__ = [
'Member',
]
-from sqlalchemy import Column, ForeignKey, Integer, Unicode
-from sqlalchemy.orm import relationship
-from zope.component import getUtility
-from zope.event import notify
-from zope.interface import implementer
from mailman.core.constants import system_preferences
from mailman.database.model import Model
@@ -42,6 +34,11 @@ from mailman.interfaces.member import (
from mailman.interfaces.user import IUser, UnverifiedAddressError
from mailman.interfaces.usermanager import IUserManager
from mailman.utilities.uid import UniqueIDFactory
+from sqlalchemy import Column, ForeignKey, Integer, Unicode
+from sqlalchemy.orm import relationship
+from zope.component import getUtility
+from zope.event import notify
+from zope.interface import implementer
uid_factory = UniqueIDFactory(context='members')
diff --git a/src/mailman/model/message.py b/src/mailman/model/message.py
index 691861d46..105066daa 100644
--- a/src/mailman/model/message.py
+++ b/src/mailman/model/message.py
@@ -17,19 +17,16 @@
"""Model for messages."""
-from __future__ import absolute_import, print_function, unicode_literals
-
-__metaclass__ = type
__all__ = [
'Message',
]
-from sqlalchemy import Column, Integer, LargeBinary, Unicode
-from zope.interface import implementer
from mailman.database.model import Model
from mailman.database.transaction import dbconnection
from mailman.interfaces.messages import IMessage
+from sqlalchemy import Column, Integer, Unicode
+from zope.interface import implementer
@@ -42,8 +39,8 @@ class Message(Model):
id = Column(Integer, primary_key=True)
# This is a Messge-ID field representation, not a database row id.
message_id = Column(Unicode)
- message_id_hash = Column(LargeBinary)
- path = Column(LargeBinary)
+ message_id_hash = Column(Unicode)
+ path = Column(Unicode)
@dbconnection
def __init__(self, store, message_id, message_id_hash, path):
diff --git a/src/mailman/model/messagestore.py b/src/mailman/model/messagestore.py
index 12b2aef46..05069119c 100644
--- a/src/mailman/model/messagestore.py
+++ b/src/mailman/model/messagestore.py
@@ -17,9 +17,6 @@
"""Model for message stores."""
-from __future__ import absolute_import, print_function, unicode_literals
-
-__metaclass__ = type
__all__ = [
'MessageStore',
]
@@ -28,16 +25,15 @@ __all__ = [
import os
import errno
import base64
+import pickle
import hashlib
-import cPickle as pickle
-
-from zope.interface import implementer
from mailman.config import config
from mailman.database.transaction import dbconnection
from mailman.interfaces.messages import IMessageStore
from mailman.model.message import Message
from mailman.utilities.filesystem import makedirs
+from zope.interface import implementer
# It could be very bad if you have already stored files and you change this
@@ -68,8 +64,8 @@ class MessageStore:
raise ValueError(
'Message ID already exists in message store: {0}'.format(
message_id))
- shaobj = hashlib.sha1(message_id)
- hash32 = base64.b32encode(shaobj.digest())
+ shaobj = hashlib.sha1(message_id.encode('utf-8'))
+ hash32 = base64.b32encode(shaobj.digest()).decode('utf-8')
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
@@ -94,7 +90,7 @@ class MessageStore:
# them and try again.
while True:
try:
- with open(path, 'w') as fp:
+ with open(path, 'wb') as fp:
# -1 says to use the highest protocol available.
pickle.dump(message, fp, -1)
break
@@ -106,7 +102,7 @@ class MessageStore:
def _get_message(self, row):
path = os.path.join(config.MESSAGES_DIR, row.path)
- with open(path) as fp:
+ with open(path, 'rb') as fp:
return pickle.load(fp)
@dbconnection
@@ -118,11 +114,6 @@ class MessageStore:
@dbconnection
def get_message_by_hash(self, store, 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
- # bytes object. Coerce to the latter if necessary; it must be ASCII.
- if not isinstance(message_id_hash, bytes):
- message_id_hash = message_id_hash.encode('ascii')
row = store.query(Message).filter_by(
message_id_hash=message_id_hash).first()
if row is None:
diff --git a/src/mailman/model/mime.py b/src/mailman/model/mime.py
index dc6a54437..240fd6e2b 100644
--- a/src/mailman/model/mime.py
+++ b/src/mailman/model/mime.py
@@ -17,21 +17,17 @@
"""The content filter."""
-from __future__ import absolute_import, print_function, unicode_literals
-
-__metaclass__ = type
__all__ = [
'ContentFilter'
]
-from sqlalchemy import Column, ForeignKey, Integer, Unicode
-from sqlalchemy.orm import relationship
-from zope.interface import implementer
-
from mailman.database.model import Model
from mailman.database.types import Enum
from mailman.interfaces.mime import IContentFilter, FilterType
+from sqlalchemy import Column, ForeignKey, Integer, Unicode
+from sqlalchemy.orm import relationship
+from zope.interface import implementer
diff --git a/src/mailman/model/pending.py b/src/mailman/model/pending.py
index 49b12c16a..05cea4e29 100644
--- a/src/mailman/model/pending.py
+++ b/src/mailman/model/pending.py
@@ -17,33 +17,28 @@
"""Implementations of the IPendable and IPending interfaces."""
-from __future__ import absolute_import, print_function, unicode_literals
-
-__metaclass__ = type
__all__ = [
'Pended',
'Pendings',
]
+import json
import time
import random
import hashlib
from lazr.config import as_timedelta
-from sqlalchemy import (
- Column, DateTime, ForeignKey, Integer, LargeBinary, Unicode)
-from sqlalchemy.orm import relationship
-from zope.interface import implementer
-from zope.interface.verify import verifyObject
-
from mailman.config import config
from mailman.database.model import Model
from mailman.database.transaction import dbconnection
from mailman.interfaces.pending import (
IPendable, IPended, IPendedKeyValue, IPendings)
from mailman.utilities.datetime import now
-from mailman.utilities.modules import call_name
+from sqlalchemy import Column, DateTime, ForeignKey, Integer, Unicode
+from sqlalchemy.orm import relationship
+from zope.interface import implementer
+from zope.interface.verify import verifyObject
@@ -71,7 +66,7 @@ class Pended(Model):
__tablename__ = 'pended'
id = Column(Integer, primary_key=True)
- token = Column(LargeBinary)
+ token = Column(Unicode)
expiration_date = Column(DateTime)
key_values = relationship('PendedKeyValue')
@@ -108,33 +103,26 @@ class Pendings:
right_now = time.time()
x = random.random() + right_now % 1.0 + time.clock() % 1.0
# Use sha1 because it produces shorter strings.
- token = hashlib.sha1(repr(x)).hexdigest()
+ token = hashlib.sha1(repr(x).encode('utf-8')).hexdigest()
# In practice, we'll never get a duplicate, but we'll be anal
# about checking anyway.
if store.query(Pended).filter_by(token=token).count() == 0:
break
else:
- raise AssertionError('Could not find a valid pendings token')
+ raise RuntimeError('Could not find a valid pendings token')
# Create the record, and then the individual key/value pairs.
pending = Pended(
token=token,
expiration_date=now() + lifetime)
for key, value in pendable.items():
+ # Both keys and values must be strings.
if isinstance(key, bytes):
key = key.decode('utf-8')
if isinstance(value, bytes):
- value = value.decode('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.model.pending.unpack_list\1' +
- '\2'.join(value))
- keyval = PendedKeyValue(key=key, value=value)
+ # Make sure we can turn this back into a bytes.
+ value = dict(__encoding__='utf-8',
+ value=value.decode('utf-8'))
+ keyval = PendedKeyValue(key=key, value=json.dumps(value))
pending.key_values.append(keyval)
store.add(pending)
return token
@@ -155,11 +143,10 @@ class Pendings:
entries = store.query(PendedKeyValue).filter(
PendedKeyValue.pended_id == pending.id)
for keyvalue in entries:
- if keyvalue.value is not None and '\1' in keyvalue.value:
- type_name, value = keyvalue.value.split('\1', 1)
- pendable[keyvalue.key] = call_name(type_name, value)
- else:
- pendable[keyvalue.key] = keyvalue.value
+ value = json.loads(keyvalue.value)
+ if isinstance(value, dict) and '__encoding__' in value:
+ value = value['value'].encode(value['__encoding__'])
+ pendable[keyvalue.key] = value
if expunge:
store.delete(keyvalue)
if expunge:
@@ -178,8 +165,3 @@ class Pendings:
for keyvalue in q:
store.delete(keyvalue)
store.delete(pending)
-
-
-
-def unpack_list(value):
- return value.split('\2')
diff --git a/src/mailman/model/preferences.py b/src/mailman/model/preferences.py
index 1278f80b7..8cec6036e 100644
--- a/src/mailman/model/preferences.py
+++ b/src/mailman/model/preferences.py
@@ -17,23 +17,19 @@
"""Model for preferences."""
-from __future__ import absolute_import, print_function, unicode_literals
-
-__metaclass__ = type
__all__ = [
'Preferences',
]
-from sqlalchemy import Boolean, Column, Integer, Unicode
-from zope.component import getUtility
-from zope.interface import implementer
-
from mailman.database.model import Model
from mailman.database.types import Enum
from mailman.interfaces.languages import ILanguageManager
from mailman.interfaces.member import DeliveryMode, DeliveryStatus
from mailman.interfaces.preferences import IPreferences
+from sqlalchemy import Boolean, Column, Integer, Unicode
+from zope.component import getUtility
+from zope.interface import implementer
diff --git a/src/mailman/model/requests.py b/src/mailman/model/requests.py
index 6b130196d..9d9692b30 100644
--- a/src/mailman/model/requests.py
+++ b/src/mailman/model/requests.py
@@ -17,25 +17,25 @@
"""Implementations of the pending requests interfaces."""
-from __future__ import absolute_import, print_function, unicode_literals
-
-__metaclass__ = type
__all__ = [
+ 'DataPendable',
+ 'ListRequests',
]
-from cPickle import dumps, loads
-from datetime import timedelta
-from sqlalchemy import Column, ForeignKey, Integer, LargeBinary, Unicode
-from sqlalchemy.orm import relationship
-from zope.component import getUtility
-from zope.interface import implementer
+import six
+from datetime import timedelta
from mailman.database.model import Model
from mailman.database.transaction import dbconnection
from mailman.database.types import Enum
from mailman.interfaces.pending import IPendable, IPendings
from mailman.interfaces.requests import IListRequests, RequestType
+from six.moves.cPickle import dumps, loads
+from sqlalchemy import Column, ForeignKey, Integer, Unicode
+from sqlalchemy.orm import relationship
+from zope.component import getUtility
+from zope.interface import implementer
@@ -50,8 +50,8 @@ class DataPendable(dict):
# such a way that it will be properly reconstituted when unpended.
clean_mapping = {}
for key, value in mapping.items():
- assert isinstance(key, basestring)
- if not isinstance(value, unicode):
+ assert isinstance(key, six.string_types)
+ if not isinstance(value, six.text_type):
key = '_pck_' + key
value = dumps(value).decode('raw-unicode-escape')
clean_mapping[key] = value
@@ -154,7 +154,7 @@ class _Request(Model):
id = Column(Integer, primary_key=True)
key = Column(Unicode)
request_type = Column(Enum(RequestType))
- data_hash = Column(LargeBinary)
+ data_hash = Column(Unicode)
mailing_list_id = Column(Integer, ForeignKey('mailinglist.id'), index=True)
mailing_list = relationship('MailingList')
diff --git a/src/mailman/model/roster.py b/src/mailman/model/roster.py
index 54bc11617..7ea3ad2a4 100644
--- a/src/mailman/model/roster.py
+++ b/src/mailman/model/roster.py
@@ -22,9 +22,6 @@ the ones that fit a particular role. These are used as the member, owner,
moderator, and administrator roster filters.
"""
-from __future__ import absolute_import, print_function, unicode_literals
-
-__metaclass__ = type
__all__ = [
'AdministratorRoster',
'DigestMemberRoster',
@@ -37,14 +34,13 @@ __all__ = [
]
-from sqlalchemy import and_, or_
-from zope.interface import implementer
-
from mailman.database.transaction import dbconnection
from mailman.interfaces.member import DeliveryMode, MemberRole
from mailman.interfaces.roster import IRoster
from mailman.model.address import Address
from mailman.model.member import Member
+from sqlalchemy import and_, or_
+from zope.interface import implementer
diff --git a/src/mailman/model/tests/test_address.py b/src/mailman/model/tests/test_address.py
index 130ec3bae..29b32f542 100644
--- a/src/mailman/model/tests/test_address.py
+++ b/src/mailman/model/tests/test_address.py
@@ -17,9 +17,6 @@
"""Test addresses."""
-from __future__ import absolute_import, print_function, unicode_literals
-
-__metaclass__ = type
__all__ = [
'TestAddress',
]
@@ -28,8 +25,11 @@ __all__ = [
import unittest
from mailman.email.validate import InvalidEmailAddressError
+from mailman.interfaces.address import ExistingAddressError
+from mailman.interfaces.usermanager import IUserManager
from mailman.model.address import Address
from mailman.testing.layers import ConfigLayer
+from zope.component import getUtility
@@ -38,6 +38,25 @@ class TestAddress(unittest.TestCase):
layer = ConfigLayer
+ def setUp(self):
+ self._usermgr = getUtility(IUserManager)
+ self._address = self._usermgr.create_address('FPERSON@example.com')
+
def test_invalid_email_string_raises_exception(self):
with self.assertRaises(InvalidEmailAddressError):
Address('not_a_valid_email_string', '')
+
+ def test_local_part_differs_only_by_case(self):
+ with self.assertRaises(ExistingAddressError) as cm:
+ self._usermgr.create_address('fperson@example.com')
+ self.assertEqual(cm.exception.address, 'FPERSON@example.com')
+
+ def test_domain_part_differs_only_by_case(self):
+ with self.assertRaises(ExistingAddressError) as cm:
+ self._usermgr.create_address('fperson@EXAMPLE.COM')
+ self.assertEqual(cm.exception.address, 'FPERSON@example.com')
+
+ def test_mixed_case_exact_match(self):
+ with self.assertRaises(ExistingAddressError) as cm:
+ self._usermgr.create_address('FPERSON@example.com')
+ self.assertEqual(cm.exception.address, 'FPERSON@example.com')
diff --git a/src/mailman/model/tests/test_bounce.py b/src/mailman/model/tests/test_bounce.py
index a22da4416..2929747bc 100644
--- a/src/mailman/model/tests/test_bounce.py
+++ b/src/mailman/model/tests/test_bounce.py
@@ -17,24 +17,21 @@
"""Test bounce model objects."""
-from __future__ import absolute_import, print_function, unicode_literals
-
-__metaclass__ = type
__all__ = [
+ 'TestBounceEvents',
]
import unittest
from datetime import datetime
-from zope.component import getUtility
-
from mailman.app.lifecycle import create_list
from mailman.database.transaction import transaction
from mailman.interfaces.bounce import BounceContext, IBounceProcessor
from mailman.testing.helpers import (
specialized_message_from_string as message_from_string)
from mailman.testing.layers import ConfigLayer
+from zope.component import getUtility
diff --git a/src/mailman/model/tests/test_domain.py b/src/mailman/model/tests/test_domain.py
index f9d1ff202..a483d9567 100644
--- a/src/mailman/model/tests/test_domain.py
+++ b/src/mailman/model/tests/test_domain.py
@@ -17,9 +17,6 @@
"""Test domains."""
-from __future__ import absolute_import, print_function, unicode_literals
-
-__metaclass__ = type
__all__ = [
'TestDomainLifecycleEvents',
'TestDomainManager',
@@ -28,8 +25,6 @@ __all__ = [
import unittest
-from zope.component import getUtility
-
from mailman.app.lifecycle import create_list
from mailman.interfaces.domain import (
DomainCreatedEvent, DomainCreatingEvent, DomainDeletedEvent,
@@ -37,6 +32,7 @@ from mailman.interfaces.domain import (
from mailman.interfaces.listmanager import IListManager
from mailman.testing.helpers import event_subscribers
from mailman.testing.layers import ConfigLayer
+from zope.component import getUtility
@@ -45,6 +41,7 @@ class TestDomainManager(unittest.TestCase):
def setUp(self):
self._events = []
+ self._manager = getUtility(IDomainManager)
def _record_event(self, event):
self._events.append(event)
@@ -53,7 +50,7 @@ class TestDomainManager(unittest.TestCase):
# Test that creating a domain in the domain manager propagates the
# expected events.
with event_subscribers(self._record_event):
- domain = getUtility(IDomainManager).add('example.org')
+ domain = self._manager.add('example.org')
self.assertEqual(len(self._events), 2)
self.assertTrue(isinstance(self._events[0], DomainCreatingEvent))
self.assertEqual(self._events[0].mail_host, 'example.org')
@@ -63,15 +60,24 @@ class TestDomainManager(unittest.TestCase):
def test_delete_domain_event(self):
# Test that deleting a domain in the domain manager propagates the
# expected event.
- domain = getUtility(IDomainManager).add('example.org')
+ domain = self._manager.add('example.org')
with event_subscribers(self._record_event):
- getUtility(IDomainManager).remove('example.org')
+ self._manager.remove('example.org')
self.assertEqual(len(self._events), 2)
self.assertTrue(isinstance(self._events[0], DomainDeletingEvent))
self.assertEqual(self._events[0].domain, domain)
self.assertTrue(isinstance(self._events[1], DomainDeletedEvent))
self.assertEqual(self._events[1].mail_host, 'example.org')
+ def test_lookup_missing_domain(self):
+ # Like dictionaries, getitem syntax raises KeyError on missing domain.
+ with self.assertRaises(KeyError):
+ self._manager['doesnotexist.com']
+
+ def test_delete_missing_domain(self):
+ # Trying to delete a missing domain gives you a KeyError.
+ self.assertRaises(KeyError, self._manager.remove, 'doesnotexist.com')
+
class TestDomainLifecycleEvents(unittest.TestCase):
diff --git a/src/mailman/model/tests/test_listmanager.py b/src/mailman/model/tests/test_listmanager.py
index b290138f3..a28698eb1 100644
--- a/src/mailman/model/tests/test_listmanager.py
+++ b/src/mailman/model/tests/test_listmanager.py
@@ -17,9 +17,6 @@
"""Test the ListManager."""
-from __future__ import absolute_import, print_function, unicode_literals
-
-__metaclass__ = type
__all__ = [
'TestListCreation',
'TestListLifecycleEvents',
@@ -29,14 +26,13 @@ __all__ = [
import unittest
-from zope.component import getUtility
-
from mailman.app.lifecycle import create_list
from mailman.app.moderator import hold_message
from mailman.config import config
+from mailman.interfaces.address import InvalidEmailAddressError
from mailman.interfaces.listmanager import (
- IListManager, ListCreatedEvent, ListCreatingEvent, ListDeletedEvent,
- ListDeletingEvent)
+ IListManager, ListAlreadyExistsError, ListCreatedEvent, ListCreatingEvent,
+ ListDeletedEvent, ListDeletingEvent)
from mailman.interfaces.messages import IMessageStore
from mailman.interfaces.requests import IListRequests
from mailman.interfaces.subscriptions import ISubscriptionService
@@ -45,6 +41,7 @@ from mailman.model.mime import ContentFilter
from mailman.testing.helpers import (
event_subscribers, specialized_message_from_string)
from mailman.testing.layers import ConfigLayer
+from zope.component import getUtility
@@ -157,11 +154,23 @@ Message-ID: <argon>
class TestListCreation(unittest.TestCase):
layer = ConfigLayer
+ def setUp(self):
+ self._manager = getUtility(IListManager)
+
def test_create_list_case_folding(self):
# LP: #1117176 describes a problem where list names created in upper
# case are not actually usable by the LMTP server.
- manager = getUtility(IListManager)
- manager.create('my-LIST@example.com')
- self.assertIsNone(manager.get('my-LIST@example.com'))
- mlist = manager.get('my-list@example.com')
+ self._manager.create('my-LIST@example.com')
+ self.assertIsNone(self._manager.get('my-LIST@example.com'))
+ mlist = self._manager.get('my-list@example.com')
self.assertEqual(mlist.list_id, 'my-list.example.com')
+
+ def test_cannot_create_a_list_twice(self):
+ self._manager.create('ant@example.com')
+ self.assertRaises(ListAlreadyExistsError,
+ self._manager.create, 'ant@example.com')
+
+ def test_list_name_must_be_fully_qualified(self):
+ with self.assertRaises(InvalidEmailAddressError) as cm:
+ self._manager.create('foo')
+ self.assertEqual(cm.exception.email, 'foo')
diff --git a/src/mailman/model/tests/test_mailinglist.py b/src/mailman/model/tests/test_mailinglist.py
index 9d6177b54..6e7c11fe6 100644
--- a/src/mailman/model/tests/test_mailinglist.py
+++ b/src/mailman/model/tests/test_mailinglist.py
@@ -17,12 +17,10 @@
"""Test MailingLists and related model objects.."""
-from __future__ import absolute_import, print_function, unicode_literals
-
-__metaclass__ = type
__all__ = [
- 'TestListArchiver',
'TestDisabledListArchiver',
+ 'TestListArchiver',
+ 'TestMailingList',
]
@@ -31,8 +29,47 @@ import unittest
from mailman.app.lifecycle import create_list
from mailman.config import config
from mailman.interfaces.mailinglist import IListArchiverSet
+from mailman.interfaces.member import (
+ AlreadySubscribedError, MemberRole, MissingPreferredAddressError)
+from mailman.interfaces.usermanager import IUserManager
from mailman.testing.helpers import configuration
from mailman.testing.layers import ConfigLayer
+from mailman.utilities.datetime import now
+from zope.component import getUtility
+
+
+
+class TestMailingList(unittest.TestCase):
+ layer = ConfigLayer
+
+ def setUp(self):
+ self._mlist = create_list('ant@example.com')
+
+ def test_no_duplicate_subscriptions(self):
+ # A user is not allowed to subscribe more than once to the mailing
+ # list with the same role.
+ anne = getUtility(IUserManager).create_user('anne@example.com')
+ # Give the user a preferred address.
+ preferred = list(anne.addresses)[0]
+ preferred.verified_on = now()
+ anne.preferred_address = preferred
+ # Subscribe Anne to the mailing list as a regular member.
+ member = self._mlist.subscribe(anne)
+ self.assertEqual(member.address, preferred)
+ self.assertEqual(member.role, MemberRole.member)
+ # A second subscription with the same role will fail.
+ with self.assertRaises(AlreadySubscribedError) as cm:
+ self._mlist.subscribe(anne)
+ self.assertEqual(cm.exception.fqdn_listname, 'ant@example.com')
+ self.assertEqual(cm.exception.email, 'anne@example.com')
+ self.assertEqual(cm.exception.role, MemberRole.member)
+
+ def test_subscribing_user_must_have_preferred_address(self):
+ # A user object cannot be subscribed to a mailing list without a
+ # preferred address.
+ anne = getUtility(IUserManager).create_user('anne@example.com')
+ self.assertRaises(MissingPreferredAddressError,
+ self._mlist.subscribe, anne)
diff --git a/src/mailman/model/tests/test_member.py b/src/mailman/model/tests/test_member.py
index 5bd3d1594..38f36acde 100644
--- a/src/mailman/model/tests/test_member.py
+++ b/src/mailman/model/tests/test_member.py
@@ -17,9 +17,6 @@
"""Test members."""
-from __future__ import absolute_import, print_function, unicode_literals
-
-__metaclass__ = type
__all__ = [
'TestMember',
]
diff --git a/src/mailman/model/tests/test_messagestore.py b/src/mailman/model/tests/test_messagestore.py
new file mode 100644
index 000000000..39d1d97ed
--- /dev/null
+++ b/src/mailman/model/tests/test_messagestore.py
@@ -0,0 +1,71 @@
+# Copyright (C) 2014 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 <http://www.gnu.org/licenses/>.
+
+"""Test the message store."""
+
+__all__ = [
+ 'TestMessageStore',
+ ]
+
+
+import unittest
+
+from mailman.interfaces.messages import IMessageStore
+from mailman.testing.helpers import (
+ specialized_message_from_string as mfs)
+from mailman.testing.layers import ConfigLayer
+from mailman.utilities.email import add_message_hash
+from zope.component import getUtility
+
+
+
+class TestMessageStore(unittest.TestCase):
+ layer = ConfigLayer
+
+ def setUp(self):
+ self._store = getUtility(IMessageStore)
+
+ def test_message_id_required(self):
+ # The Message-ID header is required in order to add it to the store.
+ message = mfs("""\
+Subject: An important message
+
+This message is very important.
+""")
+ self.assertRaises(ValueError, self._store.add, message)
+
+ def test_get_message_by_hash(self):
+ # Messages have an X-Message-ID-Hash header, the value of which can be
+ # used to look the message up in the message store.
+ message = mfs("""\
+Subject: An important message
+Message-ID: <ant>
+
+This message is very important.
+""")
+ add_message_hash(message)
+ self._store.add(message)
+ self.assertEqual(message['x-message-id-hash'],
+ 'V3YEHAFKE2WVJNK63Z7RFP4JMHISI2RG')
+ found = self._store.get_message_by_hash(
+ 'V3YEHAFKE2WVJNK63Z7RFP4JMHISI2RG')
+ self.assertEqual(found['message-id'], '<ant>')
+ self.assertEqual(found['x-message-id-hash'],
+ 'V3YEHAFKE2WVJNK63Z7RFP4JMHISI2RG')
+
+ def test_cannot_delete_missing_message(self):
+ self.assertRaises(LookupError, self._store.delete_message, 'missing')
diff --git a/src/mailman/model/tests/test_registrar.py b/src/mailman/model/tests/test_registrar.py
new file mode 100644
index 000000000..8d7c00e78
--- /dev/null
+++ b/src/mailman/model/tests/test_registrar.py
@@ -0,0 +1,64 @@
+# Copyright (C) 2014 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 <http://www.gnu.org/licenses/>.
+
+"""Test `IRegistrar`."""
+
+__all__ = [
+ 'TestRegistrar',
+ ]
+
+
+import unittest
+
+from functools import partial
+from mailman.app.lifecycle import create_list
+from mailman.interfaces.address import InvalidEmailAddressError
+from mailman.interfaces.registrar import IRegistrar
+from mailman.testing.layers import ConfigLayer
+from zope.component import getUtility
+
+
+
+class TestRegistrar(unittest.TestCase):
+ layer = ConfigLayer
+
+ def setUp(self):
+ mlist = create_list('test@example.com')
+ self._register = partial(getUtility(IRegistrar).register, mlist)
+
+ def test_invalid_empty_string(self):
+ self.assertRaises(InvalidEmailAddressError, self._register, '')
+
+ def test_invalid_space_in_name(self):
+ self.assertRaises(InvalidEmailAddressError, self._register,
+ 'some name@example.com')
+
+ def test_invalid_funky_characters(self):
+ self.assertRaises(InvalidEmailAddressError, self._register,
+ '<script>@example.com')
+
+ def test_invalid_nonascii(self):
+ self.assertRaises(InvalidEmailAddressError, self._register,
+ '\xa0@example.com')
+
+ def test_invalid_no_at_sign(self):
+ self.assertRaises(InvalidEmailAddressError, self._register,
+ 'noatsign')
+
+ def test_invalid_no_domain(self):
+ self.assertRaises(InvalidEmailAddressError, self._register,
+ 'nodom@ain')
diff --git a/src/mailman/model/tests/test_requests.py b/src/mailman/model/tests/test_requests.py
index 419c6077f..c47c61013 100644
--- a/src/mailman/model/tests/test_requests.py
+++ b/src/mailman/model/tests/test_requests.py
@@ -17,9 +17,6 @@
"""Test the various pending requests interfaces."""
-from __future__ import absolute_import, print_function, unicode_literals
-
-__metaclass__ = type
__all__ = [
'TestRequests',
]
diff --git a/src/mailman/model/tests/test_roster.py b/src/mailman/model/tests/test_roster.py
index 5bd06f485..8cf189e08 100644
--- a/src/mailman/model/tests/test_roster.py
+++ b/src/mailman/model/tests/test_roster.py
@@ -17,9 +17,6 @@
"""Test rosters."""
-from __future__ import absolute_import, print_function, unicode_literals
-
-__metaclass__ = type
__all__ = [
'TestMailingListRoster',
'TestMembershipsRoster',
@@ -28,13 +25,12 @@ __all__ = [
import unittest
-from zope.component import getUtility
-
from mailman.app.lifecycle import create_list
from mailman.interfaces.member import DeliveryMode, MemberRole
from mailman.interfaces.usermanager import IUserManager
from mailman.testing.layers import ConfigLayer
from mailman.utilities.datetime import now
+from zope.component import getUtility
diff --git a/src/mailman/model/tests/test_uid.py b/src/mailman/model/tests/test_uid.py
index 4c541205a..dd61ccc51 100644
--- a/src/mailman/model/tests/test_uid.py
+++ b/src/mailman/model/tests/test_uid.py
@@ -17,10 +17,8 @@
"""Test the UID model class."""
-from __future__ import absolute_import, print_function, unicode_literals
-
-__metaclass__ = type
__all__ = [
+ 'TestUID',
]
diff --git a/src/mailman/model/tests/test_user.py b/src/mailman/model/tests/test_user.py
index 17d4d24ff..ba5ba116f 100644
--- a/src/mailman/model/tests/test_user.py
+++ b/src/mailman/model/tests/test_user.py
@@ -17,9 +17,6 @@
"""Test users."""
-from __future__ import absolute_import, print_function, unicode_literals
-
-__metaclass__ = type
__all__ = [
'TestUser',
]
@@ -27,12 +24,14 @@ __all__ = [
import unittest
-from zope.component import getUtility
-
from mailman.app.lifecycle import create_list
+from mailman.interfaces.address import (
+ AddressAlreadyLinkedError, AddressNotLinkedError)
+from mailman.interfaces.user import UnverifiedAddressError
from mailman.interfaces.usermanager import IUserManager
from mailman.testing.layers import ConfigLayer
from mailman.utilities.datetime import now
+from zope.component import getUtility
@@ -74,3 +73,38 @@ class TestUser(unittest.TestCase):
self.assertEqual(len(emails), 2)
self.assertEqual(emails,
set(['anne@example.com', 'aperson@example.com']))
+
+ def test_uid_is_immutable(self):
+ with self.assertRaises(AttributeError):
+ self._anne.user_id = 'foo'
+
+ def test_addresses_may_only_be_linked_to_one_user(self):
+ user = getUtility(IUserManager).create_user()
+ # Anne's preferred address is already linked to her.
+ with self.assertRaises(AddressAlreadyLinkedError) as cm:
+ user.link(self._anne.preferred_address)
+ self.assertEqual(cm.exception.address, self._anne.preferred_address)
+
+ def test_unlink_from_address_not_linked_to(self):
+ # You cannot unlink an address from a user if that address is not
+ # already linked to the user.
+ user = getUtility(IUserManager).create_user()
+ with self.assertRaises(AddressNotLinkedError) as cm:
+ user.unlink(self._anne.preferred_address)
+ self.assertEqual(cm.exception.address, self._anne.preferred_address)
+
+ def test_unlink_address_which_is_not_linked(self):
+ # You cannot unlink an address which is not linked to any user.
+ address = getUtility(IUserManager).create_address('bart@example.com')
+ user = getUtility(IUserManager).create_user()
+ with self.assertRaises(AddressNotLinkedError) as cm:
+ user.unlink(address)
+ self.assertEqual(cm.exception.address, address)
+
+ def test_set_unverified_preferred_address(self):
+ # A user's preferred address cannot be set to an unverified address.
+ new_preferred = getUtility(IUserManager).create_address(
+ 'anne.person@example.com')
+ with self.assertRaises(UnverifiedAddressError) as cm:
+ self._anne.preferred_address = new_preferred
+ self.assertEqual(cm.exception.address, new_preferred)
diff --git a/src/mailman/model/uid.py b/src/mailman/model/uid.py
index 72ddd7b5a..94a4f1a17 100644
--- a/src/mailman/model/uid.py
+++ b/src/mailman/model/uid.py
@@ -17,20 +17,16 @@
"""Unique IDs."""
-from __future__ import absolute_import, print_function, unicode_literals
-
-__metaclass__ = type
__all__ = [
'UID',
]
-from sqlalchemy import Column, Integer
-
from mailman.database.model import Model
from mailman.database.transaction import dbconnection
from mailman.database.types import UUID
+from sqlalchemy import Column, Integer
diff --git a/src/mailman/model/user.py b/src/mailman/model/user.py
index ab581fdc8..a85ef0d00 100644
--- a/src/mailman/model/user.py
+++ b/src/mailman/model/user.py
@@ -17,18 +17,10 @@
"""Model for users."""
-from __future__ import absolute_import, print_function, unicode_literals
-
-__metaclass__ = type
__all__ = [
'User',
]
-from sqlalchemy import (
- Column, DateTime, ForeignKey, Integer, LargeBinary, Unicode)
-from sqlalchemy.orm import relationship, backref
-from zope.event import notify
-from zope.interface import implementer
from mailman.database.model import Model
from mailman.database.transaction import dbconnection
@@ -42,6 +34,10 @@ from mailman.model.preferences import Preferences
from mailman.model.roster import Memberships
from mailman.utilities.datetime import factory as date_factory
from mailman.utilities.uid import UniqueIDFactory
+from sqlalchemy import Column, DateTime, ForeignKey, Integer, Unicode
+from sqlalchemy.orm import relationship, backref
+from zope.event import notify
+from zope.interface import implementer
uid_factory = UniqueIDFactory(context='users')
@@ -56,7 +52,7 @@ class User(Model):
id = Column(Integer, primary_key=True)
display_name = Column(Unicode)
- _password = Column('password', LargeBinary)
+ _password = Column('password', Unicode)
_user_id = Column(UUID, index=True)
_created_on = Column(DateTime)
@@ -122,7 +118,7 @@ class User(Model):
def unlink(self, address):
"""See `IUser`."""
- if address.user is None:
+ if address.user is None or address.user is not self:
raise AddressNotLinkedError(address)
address.user = None
diff --git a/src/mailman/model/usermanager.py b/src/mailman/model/usermanager.py
index 726aa6120..374352033 100644
--- a/src/mailman/model/usermanager.py
+++ b/src/mailman/model/usermanager.py
@@ -17,16 +17,11 @@
"""A user manager."""
-from __future__ import absolute_import, print_function, unicode_literals
-
-__metaclass__ = type
__all__ = [
'UserManager',
]
-from zope.interface import implementer
-
from mailman.database.transaction import dbconnection
from mailman.interfaces.address import ExistingAddressError
from mailman.interfaces.usermanager import IUserManager
@@ -34,6 +29,7 @@ from mailman.model.address import Address
from mailman.model.member import Member
from mailman.model.preferences import Preferences
from mailman.model.user import User
+from zope.interface import implementer
diff --git a/src/mailman/mta/aliases.py b/src/mailman/mta/aliases.py
index 1b5f37d44..c309fb27b 100644
--- a/src/mailman/mta/aliases.py
+++ b/src/mailman/mta/aliases.py
@@ -17,17 +17,13 @@
"""Utility for generating all the aliases of a mailing list."""
-from __future__ import absolute_import, print_function, unicode_literals
-
-__metaclass__ = type
__all__ = [
'MailTransportAgentAliases',
]
-from zope.interface import implementer
-
from mailman.interfaces.mta import IMailTransportAgentAliases
+from zope.interface import implementer
SUBDESTINATIONS = (
diff --git a/src/mailman/mta/base.py b/src/mailman/mta/base.py
index 7b9180ea3..8d7ca75af 100644
--- a/src/mailman/mta/base.py
+++ b/src/mailman/mta/base.py
@@ -17,9 +17,6 @@
"""Base delivery class."""
-from __future__ import absolute_import, print_function, unicode_literals
-
-__metaclass__ = type
__all__ = [
'BaseDelivery',
'IndividualDelivery',
@@ -31,11 +28,10 @@ import socket
import logging
import smtplib
-from zope.interface import implementer
-
from mailman.config import config
from mailman.interfaces.mta import IMailTransportAgentDelivery
from mailman.mta.connection import Connection
+from zope.interface import implementer
log = logging.getLogger('mailman.smtp')
diff --git a/src/mailman/mta/bulk.py b/src/mailman/mta/bulk.py
index 4255e0c33..0dcd2cdf6 100644
--- a/src/mailman/mta/bulk.py
+++ b/src/mailman/mta/bulk.py
@@ -17,9 +17,6 @@
"""Bulk message delivery."""
-from __future__ import absolute_import, print_function, unicode_literals
-
-__metaclass__ = type
__all__ = [
'BulkDelivery',
]
@@ -108,4 +105,3 @@ class BulkDelivery(BaseDelivery):
mlist, msg, msgdata, recipients)
refused.update(chunk_refused)
return refused
-
diff --git a/src/mailman/mta/connection.py b/src/mailman/mta/connection.py
index 8cf419545..9c49e5fb0 100644
--- a/src/mailman/mta/connection.py
+++ b/src/mailman/mta/connection.py
@@ -17,9 +17,6 @@
"""MTA connections."""
-from __future__ import absolute_import, print_function, unicode_literals
-
-__metaclass__ = type
__all__ = [
'Connection',
]
diff --git a/src/mailman/mta/decorating.py b/src/mailman/mta/decorating.py
index ac99b3624..b4944d960 100644
--- a/src/mailman/mta/decorating.py
+++ b/src/mailman/mta/decorating.py
@@ -17,9 +17,6 @@
"""Individualized delivery with header/footer decorations."""
-from __future__ import absolute_import, print_function, unicode_literals
-
-__metaclass__ = type
__all__ = [
'DecoratingDelivery',
'DecoratingMixin',
diff --git a/src/mailman/mta/deliver.py b/src/mailman/mta/deliver.py
index be04a48bd..f01390397 100644
--- a/src/mailman/mta/deliver.py
+++ b/src/mailman/mta/deliver.py
@@ -17,9 +17,6 @@
"""Generic delivery."""
-from __future__ import absolute_import, print_function, unicode_literals
-
-__metaclass__ = type
__all__ = [
'deliver',
]
diff --git a/src/mailman/mta/docs/authentication.rst b/src/mailman/mta/docs/authentication.rst
index 94cd2c99e..f98c00e1f 100644
--- a/src/mailman/mta/docs/authentication.rst
+++ b/src/mailman/mta/docs/authentication.rst
@@ -60,7 +60,7 @@ But if the user name and password does not match, the connection will fail.
>>> response = bulk.deliver(
... mlist, msg, dict(recipients=['bperson@example.com']))
>>> dump_msgdata(response)
- bperson@example.com: (571, 'Bad authentication')
+ bperson@example.com: (571, b'Bad authentication')
>>> config.pop('auth')
diff --git a/src/mailman/mta/docs/bulk.rst b/src/mailman/mta/docs/bulk.rst
index f2a76229b..cd7873de1 100644
--- a/src/mailman/mta/docs/bulk.rst
+++ b/src/mailman/mta/docs/bulk.rst
@@ -332,7 +332,8 @@ recipients.
>>> failures = bulk.deliver(mlist, msg, msgdata)
>>> for address in sorted(failures):
- ... print(address, failures[address][0], failures[address][1])
+ ... print(address, failures[address][0],
+ ... failures[address][1].decode('ascii'))
aperson@example.org 500 Error: SMTPRecipientsRefused
bperson@example.org 500 Error: SMTPRecipientsRefused
cperson@example.org 500 Error: SMTPRecipientsRefused
@@ -350,7 +351,8 @@ Or there could be some other problem causing an SMTP response failure.
>>> failures = bulk.deliver(mlist, msg, msgdata)
>>> for address in sorted(failures):
- ... print(address, failures[address][0], failures[address][1])
+ ... print(address, failures[address][0],
+ ... failures[address][1].decode('ascii'))
aperson@example.org 450 Error: SMTPResponseException
bperson@example.org 450 Error: SMTPResponseException
cperson@example.org 450 Error: SMTPResponseException
@@ -361,7 +363,8 @@ Or there could be some other problem causing an SMTP response failure.
>>> failures = bulk.deliver(mlist, msg, msgdata)
>>> for address in sorted(failures):
- ... print(address, failures[address][0], failures[address][1])
+ ... print(address, failures[address][0],
+ ... failures[address][1].decode('ascii'))
aperson@example.org 500 Error: SMTPResponseException
bperson@example.org 500 Error: SMTPResponseException
cperson@example.org 500 Error: SMTPResponseException
diff --git a/src/mailman/mta/docs/connection.rst b/src/mailman/mta/docs/connection.rst
index a57a76bb9..f4e0d8107 100644
--- a/src/mailman/mta/docs/connection.rst
+++ b/src/mailman/mta/docs/connection.rst
@@ -75,30 +75,6 @@ will authenticate with the mail server after each new connection.
>>> reset()
>>> config.pop('auth')
-However, a bad user name or password generates an error.
-
- >>> config.push('auth', """
- ... [mta]
- ... smtp_user: baduser
- ... smtp_pass: badpass
- ... """)
-
- >>> connection = Connection(
- ... config.mta.smtp_host, int(config.mta.smtp_port), 0,
- ... config.mta.smtp_user, config.mta.smtp_pass)
- >>> connection.sendmail('anne@example.com', ['bart@example.com'], """\
- ... From: anne@example.com
- ... To: bart@example.com
- ... Subject: aardvarks
- ...
- ... """)
- Traceback (most recent call last):
- ...
- SMTPAuthenticationError: (571, 'Bad authentication')
-
- >>> reset()
- >>> config.pop('auth')
-
Sessions per connection
=======================
diff --git a/src/mailman/mta/exim4.py b/src/mailman/mta/exim4.py
index 1180b59eb..f25b12233 100644
--- a/src/mailman/mta/exim4.py
+++ b/src/mailman/mta/exim4.py
@@ -17,9 +17,6 @@
"""Creation/deletion hooks for the Exim4 MTA."""
-from __future__ import absolute_import, print_function, unicode_literals
-
-__metaclass__ = type
__all__ = [
'LMTP',
]
diff --git a/src/mailman/mta/null.py b/src/mailman/mta/null.py
index 7a3624b31..3b9f6322e 100644
--- a/src/mailman/mta/null.py
+++ b/src/mailman/mta/null.py
@@ -20,17 +20,13 @@
Exim one example of an MTA that Just Works.
"""
-from __future__ import absolute_import, print_function, unicode_literals
-
-__metaclass__ = type
__all__ = [
'NullMTA',
]
-from zope.interface import implementer
-
from mailman.interfaces.mta import IMailTransportAgentLifecycle
+from zope.interface import implementer
diff --git a/src/mailman/mta/personalized.py b/src/mailman/mta/personalized.py
index 967bca68a..4ea9075a3 100644
--- a/src/mailman/mta/personalized.py
+++ b/src/mailman/mta/personalized.py
@@ -17,9 +17,6 @@
"""Personalized delivery."""
-from __future__ import absolute_import, print_function, unicode_literals
-
-__metaclass__ = type
__all__ = [
'PersonalizedDelivery',
'PersonalizedMixin',
@@ -28,11 +25,10 @@ __all__ = [
from email.header import Header
from email.utils import formataddr
-from zope.component import getUtility
-
from mailman.interfaces.mailinglist import Personalization
from mailman.interfaces.usermanager import IUserManager
from mailman.mta.verp import VERPDelivery
+from zope.component import getUtility
diff --git a/src/mailman/mta/postfix.py b/src/mailman/mta/postfix.py
index bb709c6b4..f76a401fa 100644
--- a/src/mailman/mta/postfix.py
+++ b/src/mailman/mta/postfix.py
@@ -17,9 +17,6 @@
"""Creation/deletion hooks for the Postfix MTA."""
-from __future__ import absolute_import, print_function, unicode_literals
-
-__metaclass__ = type
__all__ = [
'LMTP',
]
@@ -29,16 +26,15 @@ import os
import logging
from flufl.lock import Lock
-from operator import attrgetter
-from zope.component import getUtility
-from zope.interface import implementer
-
from mailman.config import config
from mailman.config.config import external_configuration
from mailman.interfaces.listmanager import IListManager
from mailman.interfaces.mta import (
IMailTransportAgentAliases, IMailTransportAgentLifecycle)
from mailman.utilities.datetime import now
+from operator import attrgetter
+from zope.component import getUtility
+from zope.interface import implementer
log = logging.getLogger('mailman.error')
diff --git a/src/mailman/mta/tests/test_aliases.py b/src/mailman/mta/tests/test_aliases.py
index 30c57e292..8eeeef2c8 100644
--- a/src/mailman/mta/tests/test_aliases.py
+++ b/src/mailman/mta/tests/test_aliases.py
@@ -17,9 +17,6 @@
"""Test the MTA file generating utility."""
-from __future__ import absolute_import, print_function, unicode_literals
-
-__metaclass__ = type
__all__ = [
'TestAliases',
'TestPostfix',
@@ -31,13 +28,12 @@ import shutil
import tempfile
import unittest
-from zope.component import getUtility
-
from mailman.app.lifecycle import create_list
from mailman.interfaces.domain import IDomainManager
from mailman.interfaces.mta import IMailTransportAgentAliases
from mailman.mta.postfix import LMTP
from mailman.testing.layers import ConfigLayer
+from zope.component import getUtility
NL = '\n'
diff --git a/src/mailman/mta/tests/test_connection.py b/src/mailman/mta/tests/test_connection.py
new file mode 100644
index 000000000..74d0e537c
--- /dev/null
+++ b/src/mailman/mta/tests/test_connection.py
@@ -0,0 +1,51 @@
+# Copyright (C) 2014 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 <http://www.gnu.org/licenses/>.
+
+"""Test MTA connections."""
+
+__all__ = [
+ 'TestConnection',
+ ]
+
+
+import unittest
+
+from mailman.config import config
+from mailman.mta.connection import Connection
+from mailman.testing.layers import SMTPLayer
+from smtplib import SMTPAuthenticationError
+
+
+
+class TestConnection(unittest.TestCase):
+ layer = SMTPLayer
+
+ def test_authentication_error(self):
+ # Logging in to the MTA with a bad user name and password produces a
+ # 571 Bad Authentication error.
+ with self.assertRaises(SMTPAuthenticationError) as cm:
+ connection = Connection(
+ config.mta.smtp_host, int(config.mta.smtp_port), 0,
+ 'baduser', 'badpass')
+ connection.sendmail('anne@example.com', ['bart@example.com'], """\
+From: anne@example.com
+To: bart@example.com
+Subject: aardvarks
+
+""")
+ self.assertEqual(cm.exception.smtp_code, 571)
+ self.assertEqual(cm.exception.smtp_error, b'Bad authentication')
diff --git a/src/mailman/mta/tests/test_delivery.py b/src/mailman/mta/tests/test_delivery.py
index 0a910c13d..a2960f7cc 100644
--- a/src/mailman/mta/tests/test_delivery.py
+++ b/src/mailman/mta/tests/test_delivery.py
@@ -17,9 +17,6 @@
"""Test various aspects of email delivery."""
-from __future__ import absolute_import, print_function, unicode_literals
-
-__metaclass__ = type
__all__ = [
'TestIndividualDelivery',
]
diff --git a/src/mailman/mta/verp.py b/src/mailman/mta/verp.py
index c3d1d0999..2d436b8cb 100644
--- a/src/mailman/mta/verp.py
+++ b/src/mailman/mta/verp.py
@@ -17,9 +17,6 @@
"""VERP delivery."""
-from __future__ import absolute_import, print_function, unicode_literals
-
-__metaclass__ = type
__all__ = [
'VERPDelivery',
'VERPMixin',
diff --git a/src/mailman/options.py b/src/mailman/options.py
index a4f553a09..93ada95ab 100644
--- a/src/mailman/options.py
+++ b/src/mailman/options.py
@@ -17,9 +17,6 @@
"""Common argument parsing."""
-from __future__ import absolute_import, print_function, unicode_literals
-
-__metaclass__ = type
__all__ = [
'Options',
'SingleMailingListOptions',
@@ -31,18 +28,17 @@ import os
import sys
from copy import copy
-from optparse import Option, OptionParser, OptionValueError
-
from mailman.config import config
from mailman.core.i18n import _
from mailman.core.initialize import initialize
from mailman.version import MAILMAN_VERSION
+from optparse import Option, OptionParser, OptionValueError
def check_unicode(option, opt, value):
"""Check that the value is a unicode string."""
- if isinstance(value, unicode):
+ if not isinstance(value, bytes):
return value
try:
return value.decode(sys.getdefaultencoding())
diff --git a/src/mailman/rest/addresses.py b/src/mailman/rest/addresses.py
index f8516bc37..6cca24393 100644
--- a/src/mailman/rest/addresses.py
+++ b/src/mailman/rest/addresses.py
@@ -17,9 +17,6 @@
"""REST for addresses."""
-from __future__ import absolute_import, print_function,unicode_literals
-
-__metaclass__ = type
__all__ = [
'AllAddresses',
'AnAddress',
@@ -27,8 +24,7 @@ __all__ = [
]
-from operator import attrgetter
-from zope.component import getUtility
+import six
from mailman.interfaces.address import (
ExistingAddressError, InvalidEmailAddressError)
@@ -40,6 +36,8 @@ from mailman.rest.members import MemberCollection
from mailman.rest.preferences import Preferences
from mailman.rest.validator import Validator
from mailman.utilities.datetime import now
+from operator import attrgetter
+from zope.component import getUtility
@@ -168,6 +166,7 @@ class AnAddress(_AddressBase):
from mailman.rest.users import AddressUser
return AddressUser(self._address)
+
class UserAddresses(_AddressBase):
"""The addresses of a user."""
@@ -197,8 +196,8 @@ class UserAddresses(_AddressBase):
not_found(response)
return
user_manager = getUtility(IUserManager)
- validator = Validator(email=unicode,
- display_name=unicode,
+ validator = Validator(email=six.text_type,
+ display_name=six.text_type,
_optional=('display_name',))
try:
address = user_manager.create_address(**validator(request))
diff --git a/src/mailman/rest/docs/__init__.py b/src/mailman/rest/docs/__init__.py
index 2daf8a681..fcd8b41bb 100644
--- a/src/mailman/rest/docs/__init__.py
+++ b/src/mailman/rest/docs/__init__.py
@@ -17,9 +17,6 @@
"""Doctest layer setup."""
-from __future__ import absolute_import, print_function, unicode_literals
-
-__metaclass__ = type
__all__ = [
'layer',
]
diff --git a/src/mailman/rest/docs/addresses.rst b/src/mailman/rest/docs/addresses.rst
index bab2a7210..bcffd6830 100644
--- a/src/mailman/rest/docs/addresses.rst
+++ b/src/mailman/rest/docs/addresses.rst
@@ -64,13 +64,6 @@ But his address record can be accessed with the case-preserved version too.
registered_on: 2005-08-01T07:49:23
self_link: http://localhost:9001/3.0/addresses/bart.person@example.com
-A non-existent email address can't be retrieved.
-
- >>> dump_json('http://localhost:9001/3.0/addresses/nobody@example.com')
- Traceback (most recent call last):
- ...
- HTTPError: HTTP Error 404: 404 Not Found
-
When an address has a real name associated with it, this is also available in
the REST API.
@@ -168,7 +161,7 @@ The user is now created and the address is linked to it:
>>> cris.user == cris_user
True
>>> [a.email for a in cris_user.addresses]
- [u'cris@example.com']
+ ['cris@example.com']
A link to the user resource is now available as a sub-resource.
@@ -188,7 +181,7 @@ parameter to the POST request and set it to a false-equivalent value like 0:
... {'display_name': 'Anne User', 'auto_create': 0})
Traceback (most recent call last):
...
- HTTPError: HTTP Error 403: 403 Forbidden
+ urllib.error.HTTPError: HTTP Error 403: ...
A request to the `/user` sub-resource will return the linked user's
representation:
@@ -219,7 +212,7 @@ The address and the user can be unlinked by sending a DELETE request on the
>>> dump_json('http://localhost:9001/3.0/addresses/cris@example.com/user')
Traceback (most recent call last):
...
- HTTPError: HTTP Error 404: 404 Not Found
+ urllib.error.HTTPError: HTTP Error 404: ...
You can link an existing user to an address by passing the user's ID in the
POST request.
@@ -261,7 +254,7 @@ User addresses
==============
Users control addresses. The canonical URLs for these user-controlled
-addresses live in the `/addresses` namespace.
+addresses live in the ``/addresses`` namespace.
::
>>> dave = user_manager.create_user('dave@example.com', 'Dave Person')
diff --git a/src/mailman/rest/docs/basic.rst b/src/mailman/rest/docs/basic.rst
index 15ce37682..7e013f598 100644
--- a/src/mailman/rest/docs/basic.rst
+++ b/src/mailman/rest/docs/basic.rst
@@ -24,13 +24,10 @@ Credentials
When the `Authorization` header contains the proper credentials, the request
succeeds.
- >>> from base64 import b64encode
>>> from httplib2 import Http
- >>> auth = b64encode('{0}:{1}'.format(config.webservice.admin_user,
- ... config.webservice.admin_pass))
>>> headers = {
... 'Content-Type': 'application/x-www-form-urlencode',
- ... 'Authorization': 'Basic ' + auth,
+ ... 'Authorization': 'Basic cmVzdGFkbWluOnJlc3RwYXNz',
... }
>>> url = 'http://localhost:9001/3.0/system/versions'
>>> response, content = Http().request(url, 'GET', None, headers)
diff --git a/src/mailman/rest/docs/domains.rst b/src/mailman/rest/docs/domains.rst
index b28326f73..a78dacd85 100644
--- a/src/mailman/rest/docs/domains.rst
+++ b/src/mailman/rest/docs/domains.rst
@@ -228,13 +228,5 @@ Domains can also be deleted via the API.
server: ...
status: 204
-It is an error to delete a domain twice.
-
- >>> dump_json('http://localhost:9001/3.0/domains/lists.example.com',
- ... method='DELETE')
- Traceback (most recent call last):
- ...
- HTTPError: HTTP Error 404: 404 Not Found
-
.. _Domains: ../../model/docs/domains.html
diff --git a/src/mailman/rest/docs/helpers.rst b/src/mailman/rest/docs/helpers.rst
index 5bcf5cad4..5614e6544 100644
--- a/src/mailman/rest/docs/helpers.rst
+++ b/src/mailman/rest/docs/helpers.rst
@@ -45,7 +45,7 @@ gets modified to contain the etag under the ``http_etag`` key.
>>> resource = dict(geddy='bass', alex='guitar', neil='drums')
>>> json_data = etag(resource)
>>> print(resource['http_etag'])
- "96e036d66248cab746b7d97047e08896fcfb2493"
+ "6929ecfbda2282980a4818fb75f82e812077f77a"
For convenience, the etag function also returns the JSON representation of the
dictionary after tagging, since that's almost always what you want.
@@ -58,7 +58,7 @@ dictionary after tagging, since that's almost always what you want.
>>> dump_msgdata(data)
alex : guitar
geddy : bass
- http_etag: "96e036d66248cab746b7d97047e08896fcfb2493"
+ http_etag: "6929ecfbda2282980a4818fb75f82e812077f77a"
neil : drums
@@ -69,8 +69,9 @@ Another helper unpacks ``POST`` and ``PUT`` request variables, validating and
converting their values.
::
+ >>> import six
>>> from mailman.rest.validator import Validator
- >>> validator = Validator(one=int, two=unicode, three=bool)
+ >>> validator = Validator(one=int, two=six.text_type, three=bool)
>>> class FakeRequest:
... params = None
@@ -81,7 +82,7 @@ On valid input, the validator can be used as a ``**keyword`` argument.
>>> def print_request(one, two, three):
... print(repr(one), repr(two), repr(three))
>>> print_request(**validator(FakeRequest))
- 1 u'two' True
+ 1 'two' True
On invalid input, an exception is raised.
@@ -119,7 +120,7 @@ Extra keys are also not allowed.
However, if optional keys are missing, it's okay.
::
- >>> validator = Validator(one=int, two=unicode, three=bool,
+ >>> validator = Validator(one=int, two=six.text_type, three=bool,
... four=int, five=int,
... _optional=('four', 'five'))
@@ -128,15 +129,15 @@ However, if optional keys are missing, it's okay.
>>> def print_request(one, two, three, four=None, five=None):
... print(repr(one), repr(two), repr(three), repr(four), repr(five))
>>> print_request(**validator(FakeRequest))
- 1 u'two' True 4 5
+ 1 'two' True 4 5
>>> del FakeRequest.params['four']
>>> print_request(**validator(FakeRequest))
- 1 u'two' True None 5
+ 1 'two' True None 5
>>> del FakeRequest.params['five']
>>> print_request(**validator(FakeRequest))
- 1 u'two' True None None
+ 1 'two' True None None
But if the optional values are present, they must of course also be valid.
diff --git a/src/mailman/rest/docs/membership.rst b/src/mailman/rest/docs/membership.rst
index 30e69d9f5..b0b884d51 100644
--- a/src/mailman/rest/docs/membership.rst
+++ b/src/mailman/rest/docs/membership.rst
@@ -572,7 +572,7 @@ Elly is now a known user, and a member of the mailing list.
<User "Elly Person" (...) at ...>
>>> set(member.list_id for member in elly.memberships.members)
- set([u'ant.example.com'])
+ {'ant.example.com'}
>>> dump_json('http://localhost:9001/3.0/members')
entry 0:
@@ -674,7 +674,7 @@ so she leaves from the mailing list.
Elly is no longer a member of the mailing list.
>>> set(member.mailing_list for member in elly.memberships.members)
- set([])
+ set()
Digest delivery
diff --git a/src/mailman/rest/docs/moderation.rst b/src/mailman/rest/docs/moderation.rst
index 6e2dbb43c..6aec921f0 100644
--- a/src/mailman/rest/docs/moderation.rst
+++ b/src/mailman/rest/docs/moderation.rst
@@ -141,13 +141,6 @@ The held message can be discarded.
server: ...
status: 204
-After which, the message is gone from the moderation queue.
-
- >>> dump_json(url(request_id))
- Traceback (most recent call last):
- ...
- HTTPError: HTTP Error 404: 404 Not Found
-
Messages can also be accepted via the REST API. Let's hold a new message for
moderation.
::
diff --git a/src/mailman/rest/docs/preferences.rst b/src/mailman/rest/docs/preferences.rst
index b9332c954..172a9bedd 100644
--- a/src/mailman/rest/docs/preferences.rst
+++ b/src/mailman/rest/docs/preferences.rst
@@ -162,7 +162,7 @@ deleted.
>>> dump_json('http://localhost:9001/3.0/addresses/anne@example.com'
... '/preferences')
acknowledge_posts: True
- http_etag: "1ff07b0367bede79ade27d217e12df3915aaee2b"
+ http_etag: "..."
preferred_language: ja
self_link: http://localhost:9001/3.0/addresses/anne@example.com/preferences
diff --git a/src/mailman/rest/docs/queues.rst b/src/mailman/rest/docs/queues.rst
new file mode 100644
index 000000000..861b6806f
--- /dev/null
+++ b/src/mailman/rest/docs/queues.rst
@@ -0,0 +1,174 @@
+======
+Queues
+======
+
+You can get information about what messages are currently in the Mailman
+queues by querying the top-level ``queues`` resource. Of course, this
+information may be out-of-date by the time you receive a response, since queue
+management is asynchronous, but the information will be as current as
+possible.
+
+You can get the list of all queue names.
+
+ >>> dump_json('http://localhost:9001/3.0/queues')
+ entry 0:
+ count: 0
+ directory: .../queue/archive
+ files: []
+ http_etag: ...
+ name: archive
+ self_link: http://localhost:9001/3.0/queues/archive
+ entry 1:
+ count: 0
+ directory: .../queue/bad
+ files: []
+ http_etag: ...
+ name: bad
+ self_link: http://localhost:9001/3.0/queues/bad
+ entry 2:
+ count: 0
+ directory: .../queue/bounces
+ files: []
+ http_etag: ...
+ name: bounces
+ self_link: http://localhost:9001/3.0/queues/bounces
+ entry 3:
+ count: 0
+ directory: .../queue/command
+ files: []
+ http_etag: ...
+ name: command
+ self_link: http://localhost:9001/3.0/queues/command
+ entry 4:
+ count: 0
+ directory: .../queue/digest
+ files: []
+ http_etag: ...
+ name: digest
+ self_link: http://localhost:9001/3.0/queues/digest
+ entry 5:
+ count: 0
+ directory: .../queue/in
+ files: []
+ http_etag: ...
+ name: in
+ self_link: http://localhost:9001/3.0/queues/in
+ entry 6:
+ count: 0
+ directory: .../queue/nntp
+ files: []
+ http_etag: ...
+ name: nntp
+ self_link: http://localhost:9001/3.0/queues/nntp
+ entry 7:
+ count: 0
+ directory: .../queue/out
+ files: []
+ http_etag: ...
+ name: out
+ self_link: http://localhost:9001/3.0/queues/out
+ entry 8:
+ count: 0
+ directory: .../queue/pipeline
+ files: []
+ http_etag: ...
+ name: pipeline
+ self_link: http://localhost:9001/3.0/queues/pipeline
+ entry 9:
+ count: 0
+ directory: .../queue/retry
+ files: []
+ http_etag: ...
+ name: retry
+ self_link: http://localhost:9001/3.0/queues/retry
+ entry 10:
+ count: 0
+ directory: .../queue/shunt
+ files: []
+ http_etag: ...
+ name: shunt
+ self_link: http://localhost:9001/3.0/queues/shunt
+ entry 11:
+ count: 0
+ directory: .../queue/virgin
+ files: []
+ http_etag: ...
+ name: virgin
+ self_link: http://localhost:9001/3.0/queues/virgin
+ http_etag: ...
+ self_link: http://localhost:9001/3.0/queues
+ start: 0
+ total_size: 12
+
+Query an individual queue to get a count of, and the list of file base names
+in the queue. There are currently no files in the ``bad`` queue.
+
+ >>> dump_json('http://localhost:9001/3.0/queues/bad')
+ count: 0
+ directory: .../queue/bad
+ files: []
+ http_etag: ...
+ name: bad
+ self_link: http://localhost:9001/3.0/queues/bad
+
+We can inject a message into the ``bad`` queue. It must be destined for an
+existing mailing list.
+
+ >>> dump_json('http://localhost:9001/3.0/lists', {
+ ... 'fqdn_listname': 'ant@example.com',
+ ... })
+ content-length: 0
+ date: ...
+ location: http://localhost:9001/3.0/lists/ant.example.com
+ server: WSGIServer/0.2 CPython/3.4.2
+ status: 201
+
+While list creation takes an FQDN list name, injecting a message to the queue
+requires a List ID.
+
+ >>> dump_json('http://localhost:9001/3.0/queues/bad', {
+ ... 'list_id': 'ant.example.com',
+ ... 'text': """\
+ ... From: anne@example.com
+ ... To: ant@example.com
+ ... Subject: Testing
+ ...
+ ... """})
+ content-length: 0
+ date: ...
+ location: http://localhost:9001/3.0/queues/bad/...
+ server: ...
+ status: 201
+
+And now the ``bad`` queue has at least one message in it.
+
+ >>> dump_json('http://localhost:9001/3.0/queues/bad')
+ count: 1
+ directory: .../queue/bad
+ files: ['...']
+ http_etag: ...
+ name: bad
+ self_link: http://localhost:9001/3.0/queues/bad
+
+We can delete the injected message.
+
+ >>> json = call_http('http://localhost:9001/3.0/queues/bad')
+ >>> len(json['files'])
+ 1
+ >>> dump_json('http://localhost:9001/3.0/queues/bad/{}'.format(
+ ... json['files'][0]),
+ ... method='DELETE')
+ content-length: 0
+ date: ...
+ server: ...
+ status: 204
+
+And now the queue has no files.
+
+ >>> dump_json('http://localhost:9001/3.0/queues/bad')
+ count: 0
+ directory: .../queue/bad
+ files: []
+ http_etag: ...
+ name: bad
+ self_link: http://localhost:9001/3.0/queues/bad
diff --git a/src/mailman/rest/docs/users.rst b/src/mailman/rest/docs/users.rst
index b2adcaccb..824492333 100644
--- a/src/mailman/rest/docs/users.rst
+++ b/src/mailman/rest/docs/users.rst
@@ -277,27 +277,6 @@ Users can also be deleted via the API.
server: ...
status: 204
-Cris's resource cannot be retrieved either by email address...
-
- >>> dump_json('http://localhost:9001/3.0/users/cris@example.com')
- Traceback (most recent call last):
- ...
- HTTPError: HTTP Error 404: 404 Not Found
-
-...or user id.
-
- >>> dump_json('http://localhost:9001/3.0/users/3')
- Traceback (most recent call last):
- ...
- HTTPError: HTTP Error 404: 404 Not Found
-
-Cris's address records no longer exist either.
-
- >>> dump_json('http://localhost:9001/3.0/addresses/cris@example.com')
- Traceback (most recent call last):
- ...
- HTTPError: HTTP Error 404: 404 Not Found
-
User addresses
==============
@@ -420,12 +399,3 @@ This time, Elly successfully logs into Mailman.
date: ...
server: ...
status: 204
-
-But this time, she is unsuccessful.
-
- >>> dump_json('http://localhost:9001/3.0/users/5/login', {
- ... 'cleartext_password': 'not-the-password',
- ... }, method='POST')
- Traceback (most recent call last):
- ...
- HTTPError: HTTP Error 403: 403 Forbidden
diff --git a/src/mailman/rest/domains.py b/src/mailman/rest/domains.py
index 5d36dcab9..9bc0edf6a 100644
--- a/src/mailman/rest/domains.py
+++ b/src/mailman/rest/domains.py
@@ -17,15 +17,14 @@
"""REST for domains."""
-from __future__ import absolute_import, print_function, unicode_literals
-
-__metaclass__ = type
__all__ = [
'ADomain',
'AllDomains',
]
+import six
+
from mailman.interfaces.domain import (
BadDomainSpecificationError, IDomainManager)
from mailman.rest.helpers import (
@@ -99,10 +98,10 @@ class AllDomains(_DomainBase):
"""Create a new domain."""
domain_manager = getUtility(IDomainManager)
try:
- validator = Validator(mail_host=unicode,
- description=unicode,
- base_url=unicode,
- contact_address=unicode,
+ validator = Validator(mail_host=six.text_type,
+ description=six.text_type,
+ base_url=six.text_type,
+ contact_address=six.text_type,
_optional=('description', 'base_url',
'contact_address'))
domain = domain_manager.add(**validator(request))
diff --git a/src/mailman/rest/helpers.py b/src/mailman/rest/helpers.py
index 0bc312b1f..a39d6ceb3 100644
--- a/src/mailman/rest/helpers.py
+++ b/src/mailman/rest/helpers.py
@@ -17,9 +17,6 @@
"""Web service helpers."""
-from __future__ import absolute_import, print_function, unicode_literals
-
-__metaclass__ = type
__all__ = [
'BadRequest',
'ChildError',
@@ -59,7 +56,7 @@ def path_to(resource):
:return: The full path to the resource.
:rtype: bytes
"""
- return b'{0}://{1}:{2}/{3}/{4}'.format(
+ return '{0}://{1}:{2}/{3}/{4}'.format(
('https' if as_boolean(config.webservice.use_https) else 'http'),
config.webservice.hostname,
config.webservice.port,
@@ -107,8 +104,10 @@ def etag(resource):
assert 'http_etag' not in resource, 'Resource already etagged'
# Calculate the tag from a predictable (i.e. sorted) representation of the
# dictionary. The actual details aren't so important. pformat() is
- # guaranteed to sort the keys.
- etag = hashlib.sha1(pformat(resource)).hexdigest()
+ # guaranteed to sort the keys, however it returns a str and the hash
+ # library requires a bytes. Use the safest possible encoding.
+ hashfood = pformat(resource).encode('raw-unicode-escape')
+ etag = hashlib.sha1(hashfood).hexdigest()
resource['http_etag'] = '"{0}"'.format(etag)
return json.dumps(resource, cls=ExtendedEncoder)
diff --git a/src/mailman/rest/listconf.py b/src/mailman/rest/listconf.py
index b432268c7..6cf54a00e 100644
--- a/src/mailman/rest/listconf.py
+++ b/src/mailman/rest/listconf.py
@@ -17,14 +17,13 @@
"""Mailing list configuration via REST API."""
-from __future__ import absolute_import, print_function, unicode_literals
-
-__metaclass__ = type
__all__ = [
'ListConfiguration',
]
+import six
+
from lazr.config import as_boolean, as_timedelta
from mailman.config import config
from mailman.core.errors import (
@@ -61,7 +60,7 @@ class AcceptableAliases(GetterSetter):
alias_set = IAcceptableAliasSet(mlist)
alias_set.clear()
for alias in value:
- alias_set.add(unicode(alias))
+ alias_set.add(alias)
@@ -71,13 +70,16 @@ class AcceptableAliases(GetterSetter):
def pipeline_validator(pipeline_name):
"""Convert the pipeline name to a string, but only if it's known."""
if pipeline_name in config.pipelines:
- return unicode(pipeline_name)
+ return pipeline_name
raise ValueError('Unknown pipeline: {}'.format(pipeline_name))
-def list_of_unicode(values):
+def list_of_str(values):
"""Turn a list of things into a list of unicodes."""
- return [unicode(value) for value in values]
+ for value in values:
+ if not isinstance(value, str):
+ raise ValueError('Expected str, got {!r}'.format(value))
+ return values
@@ -96,7 +98,7 @@ def list_of_unicode(values):
# (e.g. datetimes, timedeltas, enums).
ATTRIBUTES = dict(
- acceptable_aliases=AcceptableAliases(list_of_unicode),
+ acceptable_aliases=AcceptableAliases(list_of_str),
admin_immed_notify=GetterSetter(as_boolean),
admin_notify_mchanges=GetterSetter(as_boolean),
administrivia=GetterSetter(as_boolean),
@@ -106,9 +108,9 @@ ATTRIBUTES = dict(
autorespond_postings=GetterSetter(enum_validator(ResponseAction)),
autorespond_requests=GetterSetter(enum_validator(ResponseAction)),
autoresponse_grace_period=GetterSetter(as_timedelta),
- autoresponse_owner_text=GetterSetter(unicode),
- autoresponse_postings_text=GetterSetter(unicode),
- autoresponse_request_text=GetterSetter(unicode),
+ autoresponse_owner_text=GetterSetter(six.text_type),
+ autoresponse_postings_text=GetterSetter(six.text_type),
+ autoresponse_request_text=GetterSetter(six.text_type),
archive_policy=GetterSetter(enum_validator(ArchivePolicy)),
bounces_address=GetterSetter(None),
collapse_alternatives=GetterSetter(as_boolean),
@@ -116,7 +118,7 @@ ATTRIBUTES = dict(
created_at=GetterSetter(None),
default_member_action=GetterSetter(enum_validator(Action)),
default_nonmember_action=GetterSetter(enum_validator(Action)),
- description=GetterSetter(unicode),
+ description=GetterSetter(six.text_type),
digest_last_sent_at=GetterSetter(None),
digest_size_threshold=GetterSetter(float),
filter_content=GetterSetter(as_boolean),
@@ -135,21 +137,21 @@ ATTRIBUTES = dict(
post_id=GetterSetter(None),
posting_address=GetterSetter(None),
posting_pipeline=GetterSetter(pipeline_validator),
- display_name=GetterSetter(unicode),
+ display_name=GetterSetter(six.text_type),
reply_goes_to_list=GetterSetter(enum_validator(ReplyToMunging)),
- reply_to_address=GetterSetter(unicode),
+ reply_to_address=GetterSetter(six.text_type),
request_address=GetterSetter(None),
scheme=GetterSetter(None),
send_welcome_message=GetterSetter(as_boolean),
- subject_prefix=GetterSetter(unicode),
+ subject_prefix=GetterSetter(six.text_type),
volume=GetterSetter(None),
web_host=GetterSetter(None),
- welcome_message_uri=GetterSetter(unicode),
+ welcome_message_uri=GetterSetter(six.text_type),
)
VALIDATORS = ATTRIBUTES.copy()
-for attribute, gettersetter in VALIDATORS.items():
+for attribute, gettersetter in list(VALIDATORS.items()):
if gettersetter.decoder is None:
del VALIDATORS[attribute]
diff --git a/src/mailman/rest/lists.py b/src/mailman/rest/lists.py
index c96d5ded9..a8546b95e 100644
--- a/src/mailman/rest/lists.py
+++ b/src/mailman/rest/lists.py
@@ -17,9 +17,6 @@
"""REST for mailing lists."""
-from __future__ import absolute_import, print_function, unicode_literals
-
-__metaclass__ = type
__all__ = [
'AList',
'AllLists',
@@ -30,10 +27,9 @@ __all__ = [
]
-from lazr.config import as_boolean
-from operator import attrgetter
-from zope.component import getUtility
+import six
+from lazr.config import as_boolean
from mailman.app.lifecycle import create_list, remove_list
from mailman.config import config
from mailman.interfaces.domain import BadDomainSpecificationError
@@ -50,6 +46,8 @@ from mailman.rest.helpers import (
from mailman.rest.members import AMember, MemberCollection
from mailman.rest.moderation import HeldMessages, SubscriptionRequests
from mailman.rest.validator import Validator
+from operator import attrgetter
+from zope.component import getUtility
@@ -204,16 +202,15 @@ class AllLists(_ListBase):
def on_post(self, request, response):
"""Create a new mailing list."""
try:
- validator = Validator(fqdn_listname=unicode,
- style_name=unicode,
+ validator = Validator(fqdn_listname=six.text_type,
+ style_name=six.text_type,
_optional=('style_name',))
mlist = create_list(**validator(request))
except ListAlreadyExistsError:
bad_request(response, b'Mailing list exists')
except BadDomainSpecificationError as error:
- bad_request(
- response,
- b'Domain does not exist: {0}'.format(error.domain))
+ reason = 'Domain does not exist: {}'.format(error.domain)
+ bad_request(response, reason.encode('utf-8'))
except ValueError as error:
bad_request(response, str(error))
else:
@@ -273,7 +270,7 @@ class ArchiverGetterSetter(GetterSetter):
# attribute will contain the (bytes) name of the archiver that is
# getting a new status. value will be the representation of the new
# boolean status.
- archiver = self._archiver_set.get(attribute.decode('utf-8'))
+ archiver = self._archiver_set.get(attribute)
if archiver is None:
raise ValueError('No such archiver: {}'.format(attribute))
archiver.is_enabled = as_boolean(value)
diff --git a/src/mailman/rest/members.py b/src/mailman/rest/members.py
index 4d1c87b73..ceaf54fc2 100644
--- a/src/mailman/rest/members.py
+++ b/src/mailman/rest/members.py
@@ -17,9 +17,6 @@
"""REST for members."""
-from __future__ import absolute_import, print_function, unicode_literals
-
-__metaclass__ = type
__all__ = [
'AMember',
'AllMembers',
@@ -28,9 +25,7 @@ __all__ = [
]
-from uuid import UUID
-from operator import attrgetter
-from zope.component import getUtility
+import six
from mailman.app.membership import delete_member
from mailman.interfaces.address import InvalidEmailAddressError
@@ -47,6 +42,9 @@ from mailman.rest.helpers import (
from mailman.rest.preferences import Preferences, ReadOnlyPreferences
from mailman.rest.validator import (
Validator, enum_validator, subscriber_validator)
+from operator import attrgetter
+from uuid import UUID
+from zope.component import getUtility
@@ -176,7 +174,7 @@ class AMember(_MemberBase):
return
try:
values = Validator(
- address=unicode,
+ address=six.text_type,
delivery_mode=enum_validator(DeliveryMode),
_optional=('address', 'delivery_mode'))(request)
except ValueError as error:
@@ -207,9 +205,9 @@ class AllMembers(_MemberBase):
service = getUtility(ISubscriptionService)
try:
validator = Validator(
- list_id=unicode,
+ list_id=six.text_type,
subscriber=subscriber_validator,
- display_name=unicode,
+ display_name=six.text_type,
delivery_mode=enum_validator(DeliveryMode),
role=enum_validator(MemberRole),
_optional=('delivery_mode', 'display_name', 'role'))
@@ -256,8 +254,8 @@ class FindMembers(_MemberBase):
"""Find a member"""
service = getUtility(ISubscriptionService)
validator = Validator(
- list_id=unicode,
- subscriber=unicode,
+ list_id=six.text_type,
+ subscriber=six.text_type,
role=enum_validator(MemberRole),
_optional=('list_id', 'subscriber', 'role'))
try:
diff --git a/src/mailman/rest/moderation.py b/src/mailman/rest/moderation.py
index 0bdc50688..da182acb7 100644
--- a/src/mailman/rest/moderation.py
+++ b/src/mailman/rest/moderation.py
@@ -17,9 +17,6 @@
"""REST API for Message moderation."""
-from __future__ import absolute_import, print_function, unicode_literals
-
-__metaclass__ = type
__all__ = [
'HeldMessage',
'HeldMessages',
@@ -88,7 +85,7 @@ class _HeldMessageBase(_ModerationBase):
# resource. Others we can drop. Since we're mutating the dictionary,
# we need to make a copy of the keys. When you port this to Python 3,
# you'll need to list()-ify the .keys() dictionary view.
- for key in resource.keys():
+ for key in list(resource):
if key in ('_mod_subject', '_mod_hold_date', '_mod_reason',
'_mod_sender', '_mod_message_id'):
resource[key[5:]] = resource.pop(key)
diff --git a/src/mailman/rest/preferences.py b/src/mailman/rest/preferences.py
index b85388ec9..9eafa8d77 100644
--- a/src/mailman/rest/preferences.py
+++ b/src/mailman/rest/preferences.py
@@ -17,9 +17,6 @@
"""Preferences."""
-from __future__ import absolute_import, print_function, unicode_literals
-
-__metaclass__ = type
__all__ = [
'ReadOnlyPreferences',
'Preferences',
diff --git a/src/mailman/rest/queues.py b/src/mailman/rest/queues.py
new file mode 100644
index 000000000..f1007052e
--- /dev/null
+++ b/src/mailman/rest/queues.py
@@ -0,0 +1,129 @@
+# Copyright (C) 2015 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 <http://www.gnu.org/licenses/>.
+
+"""<api>/queues."""
+
+__all__ = [
+ 'AQueue',
+ 'AQueueFile',
+ 'AllQueues',
+ ]
+
+
+import six
+
+from mailman.config import config
+from mailman.app.inject import inject_text
+from mailman.interfaces.listmanager import IListManager
+from mailman.rest.helpers import (
+ CollectionMixin, bad_request, created, etag, no_content, not_found, okay,
+ paginate, path_to)
+from mailman.rest.validator import Validator
+from zope.component import getUtility
+
+
+
+class _QueuesBase(CollectionMixin):
+ """Shared base class for queues."""
+
+ def _resource_as_dict(self, name):
+ """See `CollectionMixin`."""
+ switchboard = config.switchboards[name]
+ files = switchboard.files
+ return dict(
+ name=switchboard.name,
+ directory=switchboard.queue_directory,
+ count=len(files),
+ files=files,
+ self_link=path_to('queues/{}'.format(name)),
+ )
+
+ @paginate
+ def _get_collection(self, request):
+ """See `CollectionMixin`."""
+ return sorted(config.switchboards)
+
+
+
+class AQueue(_QueuesBase):
+ """A single queue."""
+
+ def __init__(self, name):
+ self._name = name
+
+ def on_get(self, request, response):
+ """Return a single queue resource."""
+ if self._name not in config.switchboards:
+ not_found(response)
+ else:
+ okay(response, self._resource_as_json(self._name))
+
+ def on_post(self, request, response):
+ """Inject a message into the queue."""
+ try:
+ validator = Validator(list_id=six.text_type,
+ text=six.text_type)
+ values = validator(request)
+ except ValueError as error:
+ bad_request(response, str(error))
+ return
+ list_id = values['list_id']
+ mlist = getUtility(IListManager).get_by_list_id(list_id)
+ if mlist is None:
+ bad_request(response, 'No such list: {}'.format(list_id))
+ return
+ try:
+ filebase = inject_text(
+ mlist, values['text'], switchboard=self._name)
+ except Exception as error:
+ bad_request(response, str(error))
+ return
+ else:
+ location = path_to('queues/{}/{}'.format(self._name, filebase))
+ created(response, location)
+
+
+
+class AQueueFile:
+ def __init__(self, name, filebase):
+ self._name = name
+ self._filebase = filebase
+
+ def on_delete(self, request, response):
+ """Delete the queue file."""
+ switchboard = config.switchboards.get(self._name)
+ if switchboard is None:
+ not_found(response, 'No such queue: {}'.format(self._name))
+ return
+ try:
+ switchboard.dequeue(self._filebase)
+ except FileNotFoundError:
+ not_found(response,
+ 'No such queue file: {}'.format(self._filebase))
+ else:
+ no_content(response)
+
+
+
+class AllQueues(_QueuesBase):
+ """All queues."""
+
+ def on_get(self, request, response):
+ """<api>/queues"""
+ resource = self._make_collection(request)
+ resource['self_link'] = path_to('queues')
+ okay(response, etag(resource))
diff --git a/src/mailman/rest/root.py b/src/mailman/rest/root.py
index a3d18c201..381bec751 100644
--- a/src/mailman/rest/root.py
+++ b/src/mailman/rest/root.py
@@ -17,9 +17,6 @@
"""The root of the REST API."""
-from __future__ import absolute_import, print_function, unicode_literals
-
-__metaclass__ = type
__all__ = [
'Root',
]
@@ -28,8 +25,6 @@ __all__ = [
import falcon
from base64 import b64decode
-from zope.component import getUtility
-
from mailman.config import config
from mailman.core.constants import system_preferences
from mailman.core.system import system
@@ -41,8 +36,10 @@ from mailman.rest.helpers import (
from mailman.rest.lists import AList, AllLists, Styles
from mailman.rest.members import AMember, AllMembers, FindMembers
from mailman.rest.preferences import ReadOnlyPreferences
+from mailman.rest.queues import AQueue, AQueueFile, AllQueues
from mailman.rest.templates import TemplateFinder
from mailman.rest.users import AUser, AllUsers
+from zope.component import getUtility
@@ -66,17 +63,18 @@ class Root:
# the case where no error is raised.
if request.auth is None:
raise falcon.HTTPUnauthorized(
- b'401 Unauthorized',
- b'The REST API requires authentication')
+ '401 Unauthorized',
+ 'The REST API requires authentication')
if request.auth.startswith('Basic '):
- credentials = b64decode(request.auth[6:])
+ # b64decode() returns bytes, but we require a str.
+ credentials = b64decode(request.auth[6:]).decode('utf-8')
username, password = credentials.split(':', 1)
if (username != config.webservice.admin_user or
password != config.webservice.admin_pass):
# Not authorized.
raise falcon.HTTPUnauthorized(
- b'401 Unauthorized',
- b'User is not authorized for the REST API')
+ '401 Unauthorized',
+ 'User is not authorized for the REST API')
return TopLevel()
@@ -216,3 +214,15 @@ class TopLevel:
content_type = None
return TemplateFinder(
fqdn_listname, template, language, content_type)
+
+ @child()
+ def queues(self, request, segments):
+ """/<api>/queues[/<name>[/file]]"""
+ if len(segments) == 0:
+ return AllQueues()
+ elif len(segments) == 1:
+ return AQueue(segments[0]), []
+ elif len(segments) == 2:
+ return AQueueFile(segments[0], segments[1]), []
+ else:
+ return BadRequest(), []
diff --git a/src/mailman/rest/templates.py b/src/mailman/rest/templates.py
index 44dcdefc5..8d448a704 100644
--- a/src/mailman/rest/templates.py
+++ b/src/mailman/rest/templates.py
@@ -17,9 +17,6 @@
"""Template finder."""
-from __future__ import absolute_import, print_function, unicode_literals
-
-__metaclass__ = type
__all__ = [
'TemplateFinder',
]
diff --git a/src/mailman/rest/tests/test_addresses.py b/src/mailman/rest/tests/test_addresses.py
index bbdd7d763..65c0c1e5a 100644
--- a/src/mailman/rest/tests/test_addresses.py
+++ b/src/mailman/rest/tests/test_addresses.py
@@ -17,9 +17,6 @@
"""REST address tests."""
-from __future__ import absolute_import, print_function, unicode_literals
-
-__metaclass__ = type
__all__ = [
'TestAddresses',
]
@@ -27,15 +24,14 @@ __all__ = [
import unittest
-from urllib2 import HTTPError
-from zope.component import getUtility
-
from mailman.app.lifecycle import create_list
from mailman.database.transaction import transaction
from mailman.interfaces.usermanager import IUserManager
from mailman.testing.helpers import call_api
from mailman.testing.layers import RESTLayer
from mailman.utilities.datetime import now
+from six.moves.urllib_error import HTTPError
+from zope.component import getUtility
@@ -53,6 +49,12 @@ class TestAddresses(unittest.TestCase):
self.assertEqual(json['start'], 0)
self.assertEqual(json['total_size'], 0)
+ def test_missing_address(self):
+ # An address that isn't registered yet cannot be retrieved.
+ with self.assertRaises(HTTPError) as cm:
+ call_api('http://localhost:9001/3.0/addresses/nobody@example.com')
+ self.assertEqual(cm.exception.code, 404)
+
def test_membership_of_missing_address(self):
# Try to get the memberships of a missing address.
with self.assertRaises(HTTPError) as cm:
@@ -166,7 +168,7 @@ class TestAddresses(unittest.TestCase):
'email': 'anne@example.com',
})
self.assertEqual(cm.exception.code, 400)
- self.assertEqual(cm.exception.reason, 'Address already exists')
+ self.assertEqual(cm.exception.reason, b'Address already exists')
def test_invalid_address_bad_request(self):
# Trying to add an invalid address string returns 400.
@@ -178,7 +180,7 @@ class TestAddresses(unittest.TestCase):
'email': 'invalid_address_string'
})
self.assertEqual(cm.exception.code, 400)
- self.assertEqual(cm.exception.reason, 'Invalid email address')
+ self.assertEqual(cm.exception.reason, b'Invalid email address')
def test_empty_address_bad_request(self):
# The address is required.
@@ -189,7 +191,7 @@ class TestAddresses(unittest.TestCase):
'http://localhost:9001/3.0/users/anne@example.com/addresses',
{})
self.assertEqual(cm.exception.code, 400)
- self.assertEqual(cm.exception.reason, 'Missing parameters: email')
+ self.assertEqual(cm.exception.reason, b'Missing parameters: email')
def test_get_addresses_of_missing_user(self):
# There is no user associated with the given address.
diff --git a/src/mailman/rest/tests/test_domains.py b/src/mailman/rest/tests/test_domains.py
index 44cf11ef3..72ba4c003 100644
--- a/src/mailman/rest/tests/test_domains.py
+++ b/src/mailman/rest/tests/test_domains.py
@@ -17,9 +17,6 @@
"""REST domain tests."""
-from __future__ import absolute_import, print_function, unicode_literals
-
-__metaclass__ = type
__all__ = [
'TestDomains',
]
@@ -27,14 +24,13 @@ __all__ = [
import unittest
-from urllib2 import HTTPError
-from zope.component import getUtility
-
from mailman.app.lifecycle import create_list
from mailman.database.transaction import transaction
from mailman.interfaces.listmanager import IListManager
from mailman.testing.helpers import call_api
from mailman.testing.layers import RESTLayer
+from six.moves.urllib_error import HTTPError
+from zope.component import getUtility
@@ -65,7 +61,7 @@ class TestDomains(unittest.TestCase):
content, response = call_api(
'http://localhost:9001/3.0/domains/example.com', method='DELETE')
self.assertEqual(response.status, 204)
- self.assertEqual(getUtility(IListManager).get('ant@example.com'), None)
+ self.assertIsNone(getUtility(IListManager).get('ant@example.com'))
def test_missing_domain(self):
# You get a 404 if you try to access a nonexisting domain.
@@ -80,3 +76,14 @@ class TestDomains(unittest.TestCase):
call_api(
'http://localhost:9001/3.0/domains/does-not-exist.com/lists')
self.assertEqual(cm.exception.code, 404)
+
+ def test_double_delete(self):
+ # You cannot delete a domain twice.
+ content, response = call_api(
+ 'http://localhost:9001/3.0/domains/example.com',
+ method='DELETE')
+ self.assertEqual(response.status, 204)
+ with self.assertRaises(HTTPError) as cm:
+ call_api('http://localhost:9001/3.0/domains/example.com',
+ method='DELETE')
+ self.assertEqual(cm.exception.code, 404)
diff --git a/src/mailman/rest/tests/test_listconf.py b/src/mailman/rest/tests/test_listconf.py
index 93171ec4b..d013cdce9 100644
--- a/src/mailman/rest/tests/test_listconf.py
+++ b/src/mailman/rest/tests/test_listconf.py
@@ -17,9 +17,6 @@
"""Test list configuration via the REST API."""
-from __future__ import absolute_import, print_function, unicode_literals
-
-__metaclass__ = type
__all__ = [
'TestConfiguration',
]
diff --git a/src/mailman/rest/tests/test_lists.py b/src/mailman/rest/tests/test_lists.py
index ba6f6ea59..839fd0f58 100644
--- a/src/mailman/rest/tests/test_lists.py
+++ b/src/mailman/rest/tests/test_lists.py
@@ -17,9 +17,6 @@
"""REST list tests."""
-from __future__ import absolute_import, print_function, unicode_literals
-
-__metaclass__ = type
__all__ = [
'TestListArchivers',
'TestListPagination',
@@ -30,14 +27,13 @@ __all__ = [
import unittest
-from urllib2 import HTTPError
-from zope.component import getUtility
-
from mailman.app.lifecycle import create_list
from mailman.database.transaction import transaction
from mailman.interfaces.usermanager import IUserManager
from mailman.testing.helpers import call_api
from mailman.testing.layers import RESTLayer
+from six.moves.urllib_error import HTTPError
+from zope.component import getUtility
@@ -129,7 +125,7 @@ class TestLists(unittest.TestCase):
})
self.assertEqual(cm.exception.code, 400)
self.assertEqual(cm.exception.reason,
- 'Domain does not exist: no-domain.example.org')
+ b'Domain does not exist: no-domain.example.org')
def test_cannot_create_duplicate_list(self):
# You cannot create a list that already exists.
@@ -141,7 +137,7 @@ class TestLists(unittest.TestCase):
'fqdn_listname': 'ant@example.com',
})
self.assertEqual(cm.exception.code, 400)
- self.assertEqual(cm.exception.reason, 'Mailing list exists')
+ self.assertEqual(cm.exception.reason, b'Mailing list exists')
def test_cannot_delete_missing_list(self):
# You cannot delete a list that does not exist.
@@ -220,7 +216,7 @@ class TestListArchivers(unittest.TestCase):
method='PATCH')
self.assertEqual(cm.exception.code, 400)
self.assertEqual(cm.exception.reason,
- 'Unexpected parameters: bogus-archiver')
+ b'Unexpected parameters: bogus-archiver')
def test_put_incomplete_statuses(self):
# PUT requires the full resource representation. This one forgets to
@@ -233,7 +229,7 @@ class TestListArchivers(unittest.TestCase):
method='PUT')
self.assertEqual(cm.exception.code, 400)
self.assertEqual(cm.exception.reason,
- 'Missing parameters: mhonarc, prototype')
+ b'Missing parameters: mhonarc, prototype')
def test_patch_bogus_status(self):
# Archiver statuses must be interpretable as booleans.
@@ -246,7 +242,7 @@ class TestListArchivers(unittest.TestCase):
},
method='PATCH')
self.assertEqual(cm.exception.code, 400)
- self.assertEqual(cm.exception.reason, 'Invalid boolean value: sure')
+ self.assertEqual(cm.exception.reason, b'Invalid boolean value: sure')
diff --git a/src/mailman/rest/tests/test_membership.py b/src/mailman/rest/tests/test_membership.py
index 3c7d0520b..4ca28626f 100644
--- a/src/mailman/rest/tests/test_membership.py
+++ b/src/mailman/rest/tests/test_membership.py
@@ -17,9 +17,6 @@
"""REST membership tests."""
-from __future__ import absolute_import, print_function, unicode_literals
-
-__metaclass__ = type
__all__ = [
'TestMembership',
'TestNonmembership',
@@ -28,9 +25,6 @@ __all__ = [
import unittest
-from urllib2 import HTTPError
-from zope.component import getUtility
-
from mailman.app.lifecycle import create_list
from mailman.config import config
from mailman.database.transaction import transaction
@@ -41,6 +35,8 @@ from mailman.testing.helpers import (
from mailman.runners.incoming import IncomingRunner
from mailman.testing.layers import ConfigLayer, RESTLayer
from mailman.utilities.datetime import now
+from six.moves.urllib_error import HTTPError
+from zope.component import getUtility
@@ -60,7 +56,7 @@ class TestMembership(unittest.TestCase):
'subscriber': 'nobody@example.com',
})
self.assertEqual(cm.exception.code, 400)
- self.assertEqual(cm.exception.msg, 'No such list')
+ self.assertEqual(cm.exception.reason, b'No such list')
def test_try_to_leave_missing_list(self):
# A user tries to leave a non-existent list.
@@ -100,7 +96,7 @@ class TestMembership(unittest.TestCase):
'subscriber': 'anne@example.com',
})
self.assertEqual(cm.exception.code, 409)
- self.assertEqual(cm.exception.msg, 'Member already subscribed')
+ self.assertEqual(cm.exception.reason, b'Member already subscribed')
def test_join_with_invalid_delivery_mode(self):
with self.assertRaises(HTTPError) as cm:
@@ -111,8 +107,8 @@ class TestMembership(unittest.TestCase):
'delivery_mode': 'invalid-mode',
})
self.assertEqual(cm.exception.code, 400)
- self.assertEqual(cm.exception.msg,
- 'Cannot convert parameters: delivery_mode')
+ self.assertEqual(cm.exception.reason,
+ b'Cannot convert parameters: delivery_mode')
def test_join_email_contains_slash(self):
content, response = call_api('http://localhost:9001/3.0/members', {
@@ -204,7 +200,7 @@ class TestMembership(unittest.TestCase):
'powers': 'super',
}, method='PATCH')
self.assertEqual(cm.exception.code, 400)
- self.assertEqual(cm.exception.msg, 'Unexpected parameters: powers')
+ self.assertEqual(cm.exception.reason, b'Unexpected parameters: powers')
def test_member_all_without_preferences(self):
# /members/<id>/all should return a 404 when it isn't trailed by
diff --git a/src/mailman/rest/tests/test_moderation.py b/src/mailman/rest/tests/test_moderation.py
index c0ec4755a..2b72b91eb 100644
--- a/src/mailman/rest/tests/test_moderation.py
+++ b/src/mailman/rest/tests/test_moderation.py
@@ -17,17 +17,13 @@
"""REST moderation tests."""
-from __future__ import absolute_import, print_function, unicode_literals
-
-__metaclass__ = type
__all__ = [
+ 'TestModeration',
]
import unittest
-from urllib2 import HTTPError
-
from mailman.app.lifecycle import create_list
from mailman.app.moderator import hold_message, hold_subscription
from mailman.config import config
@@ -36,6 +32,7 @@ from mailman.interfaces.member import DeliveryMode
from mailman.testing.helpers import (
call_api, specialized_message_from_string as mfs)
from mailman.testing.layers import RESTLayer
+from six.moves.urllib_error import HTTPError
@@ -97,7 +94,8 @@ Something else.
with self.assertRaises(HTTPError) as cm:
call_api(url.format(held_id), {'action': 'bogus'})
self.assertEqual(cm.exception.code, 400)
- self.assertEqual(cm.exception.msg, 'Cannot convert parameters: action')
+ self.assertEqual(cm.exception.msg,
+ b'Cannot convert parameters: action')
def test_bad_subscription_request_id(self):
# Bad request when request_id is not an integer.
@@ -123,4 +121,18 @@ Something else.
with self.assertRaises(HTTPError) as cm:
call_api(url.format(held_id), {'action': 'bogus'})
self.assertEqual(cm.exception.code, 400)
- self.assertEqual(cm.exception.msg, 'Cannot convert parameters: action')
+ self.assertEqual(cm.exception.msg,
+ b'Cannot convert parameters: action')
+
+ def test_discard(self):
+ # Discarding a message removes it from the moderation queue.
+ with transaction():
+ held_id = hold_message(self._mlist, self._msg)
+ url = 'http://localhost:9001/3.0/lists/ant@example.com/held/{}'.format(
+ held_id)
+ content, response = call_api(url, dict(action='discard'))
+ self.assertEqual(response.status, 204)
+ # Now it's gone.
+ with self.assertRaises(HTTPError) as cm:
+ call_api(url, dict(action='discard'))
+ self.assertEqual(cm.exception.code, 404)
diff --git a/src/mailman/rest/tests/test_paginate.py b/src/mailman/rest/tests/test_paginate.py
index e267100c7..a482c7007 100644
--- a/src/mailman/rest/tests/test_paginate.py
+++ b/src/mailman/rest/tests/test_paginate.py
@@ -17,9 +17,6 @@
"""paginate helper tests."""
-from __future__ import absolute_import, print_function, unicode_literals
-
-__metaclass__ = type
__all__ = [
'TestPaginateHelper',
]
diff --git a/src/mailman/rest/tests/test_preferences.py b/src/mailman/rest/tests/test_preferences.py
index 91a066cff..6d34d7763 100644
--- a/src/mailman/rest/tests/test_preferences.py
+++ b/src/mailman/rest/tests/test_preferences.py
@@ -17,9 +17,6 @@
"""Test various preference functionality."""
-from __future__ import absolute_import, print_function, unicode_literals
-
-__metaclass__ = type
__all__ = [
'TestPreferences',
]
@@ -32,10 +29,11 @@ from mailman.database.transaction import transaction
from mailman.interfaces.usermanager import IUserManager
from mailman.testing.helpers import call_api
from mailman.testing.layers import RESTLayer
-from urllib2 import HTTPError
+from six.moves.urllib_error import HTTPError
from zope.component import getUtility
+
class TestPreferences(unittest.TestCase):
"""Test various preference functionality."""
diff --git a/src/mailman/rest/tests/test_queues.py b/src/mailman/rest/tests/test_queues.py
new file mode 100644
index 000000000..43659a2e4
--- /dev/null
+++ b/src/mailman/rest/tests/test_queues.py
@@ -0,0 +1,107 @@
+# Copyright (C) 2015 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 <http://www.gnu.org/licenses/>.
+
+"""Test the `queues` resource."""
+
+__all__ = [
+ 'TestQueues',
+ ]
+
+
+import unittest
+
+from mailman.app.lifecycle import create_list
+from mailman.config import config
+from mailman.database.transaction import transaction
+from mailman.testing.helpers import call_api, get_queue_messages
+from mailman.testing.layers import RESTLayer
+from six.moves.urllib_error import HTTPError
+
+
+TEXT = """\
+From: anne@example.com
+To: test@example.com
+Subject: A test
+Message-ID: <ant>
+
+"""
+
+
+
+class TestQueues(unittest.TestCase):
+ layer = RESTLayer
+
+ def setUp(self):
+ with transaction():
+ self._mlist = create_list('test@example.com')
+
+ def test_missing_queue(self):
+ # Trying to print a missing queue gives a 404.
+ with self.assertRaises(HTTPError) as cm:
+ call_api('http://localhost:9001/3.0/queues/notaq')
+ self.assertEqual(cm.exception.code, 404)
+
+ def test_no_such_list(self):
+ # POSTing to a queue with a bad list-id gives a 400.
+ with self.assertRaises(HTTPError) as cm:
+ call_api('http://localhost:9001/3.0/queues/bad', {
+ 'list_id': 'nosuchlist.example.com',
+ 'text': TEXT,
+ })
+ self.assertEqual(cm.exception.code, 400)
+
+ def test_inject(self):
+ # Injecting a message leaves the message in the queue.
+ starting_messages = get_queue_messages('bad')
+ self.assertEqual(len(starting_messages), 0)
+ content, response = call_api('http://localhost:9001/3.0/queues/bad', {
+ 'list_id': 'test.example.com',
+ 'text': TEXT})
+ self.assertEqual(response.status, 201)
+ location = response['location']
+ filebase = location.split('/')[-1]
+ # The message is in the 'bad' queue.
+ content, response = call_api('http://localhost:9001/3.0/queues/bad')
+ files = content['files']
+ self.assertEqual(len(files), 1)
+ self.assertEqual(files[0], filebase)
+ # Verify the files directly.
+ files = list(config.switchboards['bad'].files)
+ self.assertEqual(len(files), 1)
+ self.assertEqual(files[0], filebase)
+ # Verify the content.
+ items = get_queue_messages('bad')
+ self.assertEqual(len(items), 1)
+ msg = items[0].msg
+ # Remove some headers that get added by Mailman.
+ del msg['date']
+ self.assertEqual(msg['x-message-id-hash'],
+ 'MS6QLWERIJLGCRF44J7USBFDELMNT2BW')
+ del msg['x-message-id-hash']
+ self.assertMultiLineEqual(msg.as_string(), TEXT)
+
+ def test_delete_file(self):
+ # Inject a file, then delete it.
+ content, response = call_api('http://localhost:9001/3.0/queues/bad', {
+ 'list_id': 'test.example.com',
+ 'text': TEXT})
+ location = response['location']
+ self.assertEqual(len(config.switchboards['bad'].files), 1)
+ # Delete the file through REST.
+ content, response = call_api(location, method='DELETE')
+ self.assertEqual(response.status, 204)
+ self.assertEqual(len(config.switchboards['bad'].files), 0)
diff --git a/src/mailman/rest/tests/test_root.py b/src/mailman/rest/tests/test_root.py
index 510120087..59cd93637 100644
--- a/src/mailman/rest/tests/test_root.py
+++ b/src/mailman/rest/tests/test_root.py
@@ -17,9 +17,6 @@
"""REST root object tests."""
-from __future__ import absolute_import, print_function, unicode_literals
-
-__metaclass__ = type
__all__ = [
'TestRoot',
]
@@ -35,7 +32,7 @@ from mailman.config import config
from mailman.core.system import system
from mailman.testing.helpers import call_api
from mailman.testing.layers import RESTLayer
-from urllib2 import HTTPError
+from six.moves.urllib_error import HTTPError
@@ -106,22 +103,23 @@ class TestRoot(unittest.TestCase):
}
response, raw_content = Http().request(url, 'GET', None, headers)
self.assertEqual(response.status, 401)
- content = json.loads(raw_content)
+ content = json.loads(raw_content.decode('utf-8'))
self.assertEqual(content['title'], '401 Unauthorized')
self.assertEqual(content['description'],
'The REST API requires authentication')
def test_unauthorized(self):
# Bad Basic Auth credentials results in a 401 error.
- auth = b64encode('baduser:badpass')
+ userpass = b64encode(b'baduser:badpass')
+ auth = 'Basic {}'.format(userpass.decode('ascii'))
url = 'http://localhost:9001/3.0/system'
headers = {
'Content-Type': 'application/x-www-form-urlencode',
- 'Authorization': 'Basic ' + auth,
+ 'Authorization': auth,
}
response, raw_content = Http().request(url, 'GET', None, headers)
self.assertEqual(response.status, 401)
- content = json.loads(raw_content)
+ content = json.loads(raw_content.decode('utf-8'))
self.assertEqual(content['title'], '401 Unauthorized')
self.assertEqual(content['description'],
'User is not authorized for the REST API')
diff --git a/src/mailman/rest/tests/test_systemconf.py b/src/mailman/rest/tests/test_systemconf.py
index 2eb4fa251..2158a024a 100644
--- a/src/mailman/rest/tests/test_systemconf.py
+++ b/src/mailman/rest/tests/test_systemconf.py
@@ -128,6 +128,7 @@ class TestSystemConfiguration(unittest.TestCase):
'passwords',
'paths.dev',
'paths.fhs',
+ 'paths.here',
'paths.local',
'paths.testing',
'runner.archive',
diff --git a/src/mailman/rest/tests/test_users.py b/src/mailman/rest/tests/test_users.py
index 10cc724a3..b4bd50330 100644
--- a/src/mailman/rest/tests/test_users.py
+++ b/src/mailman/rest/tests/test_users.py
@@ -17,9 +17,6 @@
"""REST user tests."""
-from __future__ import absolute_import, print_function, unicode_literals
-
-__metaclass__ = type
__all__ = [
'TestLP1074374',
'TestLogin',
@@ -30,15 +27,14 @@ __all__ = [
import os
import unittest
-from urllib2 import HTTPError
-from zope.component import getUtility
-
from mailman.app.lifecycle import create_list
from mailman.config import config
from mailman.database.transaction import transaction
from mailman.interfaces.usermanager import IUserManager
from mailman.testing.helpers import call_api, configuration
from mailman.testing.layers import RESTLayer
+from six.moves.urllib_error import HTTPError
+from zope.component import getUtility
@@ -108,6 +104,48 @@ class TestUsers(unittest.TestCase):
method='DELETE')
self.assertEqual(cm.exception.code, 404)
+ def test_delete_user_twice(self):
+ # You cannot DELETE a user twice, either by address or user id.
+ with transaction():
+ anne = getUtility(IUserManager).create_user(
+ 'anne@example.com', 'Anne Person')
+ user_id = anne.user_id
+ content, response = call_api(
+ 'http://localhost:9001/3.0/users/anne@example.com',
+ method='DELETE')
+ self.assertEqual(response.status, 204)
+ with self.assertRaises(HTTPError) as cm:
+ call_api('http://localhost:9001/3.0/users/anne@example.com',
+ method='DELETE')
+ self.assertEqual(cm.exception.code, 404)
+ with self.assertRaises(HTTPError) as cm:
+ call_api('http://localhost:9001/3.0/users/{}'.format(user_id),
+ method='DELETE')
+ self.assertEqual(cm.exception.code, 404)
+
+ def test_get_after_delete(self):
+ # You cannot GET a user record after deleting them.
+ with transaction():
+ anne = getUtility(IUserManager).create_user(
+ 'anne@example.com', 'Anne Person')
+ user_id = anne.user_id
+ # You can still GET the user record.
+ content, response = call_api(
+ 'http://localhost:9001/3.0/users/anne@example.com')
+ self.assertEqual(response.status, 200)
+ # Delete the user.
+ content, response = call_api(
+ 'http://localhost:9001/3.0/users/anne@example.com',
+ method='DELETE')
+ self.assertEqual(response.status, 204)
+ # The user record can no longer be retrieved.
+ with self.assertRaises(HTTPError) as cm:
+ call_api('http://localhost:9001/3.0/users/anne@example.com')
+ self.assertEqual(cm.exception.code, 404)
+ with self.assertRaises(HTTPError) as cm:
+ call_api('http://localhost:9001/3.0/users/{}'.format(user_id))
+ self.assertEqual(cm.exception.code, 404)
+
def test_existing_user_error(self):
# Creating a user twice results in an error.
call_api('http://localhost:9001/3.0/users', {
@@ -120,7 +158,7 @@ class TestUsers(unittest.TestCase):
})
self.assertEqual(cm.exception.code, 400)
self.assertEqual(cm.exception.reason,
- 'Address already exists: anne@example.com')
+ b'Address already exists: anne@example.com')
def test_addresses_of_missing_user_id(self):
# Trying to get the /addresses of a missing user id results in error.
@@ -251,6 +289,21 @@ class TestLogin(unittest.TestCase):
'anne@example.com', 'Anne Person')
self.anne.password = config.password_context.encrypt('abc123')
+ def test_login_with_cleartext_password(self):
+ # A user can log in with the correct clear text password.
+ content, response = call_api(
+ 'http://localhost:9001/3.0/users/anne@example.com/login', {
+ 'cleartext_password': 'abc123',
+ }, method='POST')
+ self.assertEqual(response.status, 204)
+ # But the user cannot log in with an incorrect password.
+ with self.assertRaises(HTTPError) as cm:
+ call_api(
+ 'http://localhost:9001/3.0/users/anne@example.com/login', {
+ 'cleartext_password': 'not-the-password',
+ }, method='POST')
+ self.assertEqual(cm.exception.code, 403)
+
def test_wrong_parameter(self):
# A bad request because it is mistyped the required attribute.
with self.assertRaises(HTTPError) as cm:
diff --git a/src/mailman/rest/users.py b/src/mailman/rest/users.py
index 7ab1d6818..175c1f76c 100644
--- a/src/mailman/rest/users.py
+++ b/src/mailman/rest/users.py
@@ -17,21 +17,15 @@
"""REST for users."""
-from __future__ import absolute_import, print_function, unicode_literals
-
-__metaclass__ = type
__all__ = [
'AUser',
+ 'AddressUser',
'AllUsers',
'Login',
]
from lazr.config import as_boolean
-from passlib.utils import generate_password as generate
-from uuid import UUID
-from zope.component import getUtility
-
from mailman.config import config
from mailman.core.errors import (
ReadOnlyPATCHRequestError, UnknownPATCHRequestError)
@@ -44,8 +38,12 @@ from mailman.rest.helpers import (
path_to)
from mailman.rest.preferences import Preferences
from mailman.rest.validator import PatchValidator, Validator
+from passlib.utils import generate_password as generate
+from uuid import UUID
+from zope.component import getUtility
+
# Attributes of a user which can be changed via the REST API.
class PasswordEncrypterGetterSetter(GetterSetter):
def __init__(self):
@@ -60,19 +58,20 @@ class PasswordEncrypterGetterSetter(GetterSetter):
ATTRIBUTES = dict(
- display_name=GetterSetter(unicode),
+ display_name=GetterSetter(str),
cleartext_password=PasswordEncrypterGetterSetter(),
)
CREATION_FIELDS = dict(
- email=unicode,
- display_name=unicode,
- password=unicode,
+ email=str,
+ display_name=str,
+ password=str,
_optional=('display_name', 'password'),
)
+
def create_user(arguments, response):
"""Create a new user."""
# We can't pass the 'password' argument to the user creation method, so
@@ -83,7 +82,7 @@ def create_user(arguments, response):
user = getUtility(IUserManager).create_user(**arguments)
except ExistingAddressError as error:
bad_request(
- response, b'Address already exists: {}'.format(error.address))
+ response, 'Address already exists: {}'.format(error.address))
return None
if password is None:
# This will have to be reset since it cannot be retrieved.
@@ -360,7 +359,7 @@ class Login:
# We do not want to encrypt the plaintext password given in the POST
# data. That would hash the password, but we need to have the
# plaintext in order to pass into passlib.
- validator = Validator(cleartext_password=GetterSetter(unicode))
+ validator = Validator(cleartext_password=GetterSetter(str))
try:
values = validator(request)
except ValueError as error:
diff --git a/src/mailman/rest/validator.py b/src/mailman/rest/validator.py
index cbcc5f652..017e31847 100644
--- a/src/mailman/rest/validator.py
+++ b/src/mailman/rest/validator.py
@@ -17,9 +17,6 @@
"""REST web form validation."""
-from __future__ import absolute_import, print_function, unicode_literals
-
-__metaclass__ = type
__all__ = [
'PatchValidator',
'Validator',
@@ -29,12 +26,11 @@ __all__ = [
]
-from uuid import UUID
-from zope.component import getUtility
-
from mailman.core.errors import (
ReadOnlyPATCHRequestError, UnknownPATCHRequestError)
from mailman.interfaces.languages import ILanguageManager
+from uuid import UUID
+from zope.component import getUtility
COMMASPACE = ', '
@@ -62,7 +58,7 @@ def subscriber_validator(subscriber):
try:
return UUID(int=int(subscriber))
except ValueError:
- return unicode(subscriber)
+ return subscriber
def language_validator(code):
diff --git a/src/mailman/rest/wsgiapp.py b/src/mailman/rest/wsgiapp.py
index 698c4269d..ad62244c8 100644
--- a/src/mailman/rest/wsgiapp.py
+++ b/src/mailman/rest/wsgiapp.py
@@ -17,9 +17,6 @@
"""Basic WSGI Application object for REST server."""
-from __future__ import absolute_import, print_function, unicode_literals
-
-__metaclass__ = type
__all__ = [
'make_application',
'make_server',
@@ -85,7 +82,7 @@ class RootedAPI(API):
if matcher is _missing:
continue
result = None
- if isinstance(matcher, basestring):
+ if isinstance(matcher, str):
# Is the matcher string a regular expression or plain
# string? If it starts with a caret, it's a regexp.
if matcher.startswith('^'):
diff --git a/src/mailman/rules/administrivia.py b/src/mailman/rules/administrivia.py
index 3052dcb46..866463d6c 100644
--- a/src/mailman/rules/administrivia.py
+++ b/src/mailman/rules/administrivia.py
@@ -17,20 +17,16 @@
"""The administrivia rule."""
-from __future__ import absolute_import, print_function, unicode_literals
-
-__metaclass__ = type
__all__ = [
'Administrivia',
]
from email.iterators import typed_subpart_iterator
-from zope.interface import implementer
-
from mailman.config import config
from mailman.core.i18n import _
from mailman.interfaces.rules import IRule
+from zope.interface import implementer
# The list of email commands we search for in the Subject header and payload.
@@ -74,7 +70,7 @@ class Administrivia:
# Search only the first text/plain subpart of the message. There's
# really no good way to find email commands in any other content type.
for part in typed_subpart_iterator(msg, 'text', 'plain'):
- payload = part.get_payload(decode=True)
+ payload = part.get_payload()
lines = payload.splitlines()
# Count lines without using enumerate() because blank lines in the
# payload don't count against the maximum examined.
diff --git a/src/mailman/rules/any.py b/src/mailman/rules/any.py
index e5f80fbc4..72f6da873 100644
--- a/src/mailman/rules/any.py
+++ b/src/mailman/rules/any.py
@@ -17,18 +17,14 @@
"""Check if any previous rules have matched."""
-from __future__ import absolute_import, print_function, unicode_literals
-
-__metaclass__ = type
__all__ = [
'Any',
]
-from zope.interface import implementer
-
from mailman.core.i18n import _
from mailman.interfaces.rules import IRule
+from zope.interface import implementer
diff --git a/src/mailman/rules/approved.py b/src/mailman/rules/approved.py
index 3b40d5dc9..5aa66c7df 100644
--- a/src/mailman/rules/approved.py
+++ b/src/mailman/rules/approved.py
@@ -17,9 +17,6 @@
"""Look for moderator pre-approval."""
-from __future__ import absolute_import, print_function, unicode_literals
-
-__metaclass__ = type
__all__ = [
'Approved',
]
@@ -28,11 +25,10 @@ __all__ = [
import re
from email.iterators import typed_subpart_iterator
-from zope.interface import implementer
-
from mailman.config import config
from mailman.core.i18n import _
from mailman.interfaces.rules import IRule
+from zope.interface import implementer
EMPTYSTRING = ''
@@ -113,7 +109,7 @@ class Approved:
# may not work with rtf or whatever else is possible.
pattern = header + ':(\s|&nbsp;)*' + re.escape(password)
for part in typed_subpart_iterator(msg, 'text'):
- payload = part.get_payload(decode=True)
+ payload = part.get_payload()
if payload is not None:
if re.search(pattern, payload):
reset_payload(part, re.sub(pattern, '', payload))
diff --git a/src/mailman/rules/emergency.py b/src/mailman/rules/emergency.py
index ba7abe562..a1addcdb7 100644
--- a/src/mailman/rules/emergency.py
+++ b/src/mailman/rules/emergency.py
@@ -17,18 +17,14 @@
"""The emergency hold rule."""
-from __future__ import absolute_import, print_function, unicode_literals
-
-__metaclass__ = type
__all__ = [
'Emergency',
]
-from zope.interface import implementer
-
from mailman.core.i18n import _
from mailman.interfaces.rules import IRule
+from zope.interface import implementer
diff --git a/src/mailman/rules/implicit_dest.py b/src/mailman/rules/implicit_dest.py
index 0bc229b15..9d3e6d079 100644
--- a/src/mailman/rules/implicit_dest.py
+++ b/src/mailman/rules/implicit_dest.py
@@ -17,21 +17,18 @@
"""The implicit destination rule."""
-from __future__ import absolute_import, print_function, unicode_literals
-
-__metaclass__ = type
__all__ = [
'ImplicitDestination',
]
import re
-from email.utils import getaddresses
-from zope.interface import implementer
+from email.utils import getaddresses
from mailman.core.i18n import _
from mailman.interfaces.mailinglist import IAcceptableAliasSet
from mailman.interfaces.rules import IRule
+from zope.interface import implementer
diff --git a/src/mailman/rules/loop.py b/src/mailman/rules/loop.py
index 145af8b34..30d7dde59 100644
--- a/src/mailman/rules/loop.py
+++ b/src/mailman/rules/loop.py
@@ -17,18 +17,14 @@
"""Look for a posting loop."""
-from __future__ import absolute_import, print_function, unicode_literals
-
-__metaclass__ = type
__all__ = [
'Loop',
]
-from zope.interface import implementer
-
from mailman.core.i18n import _
from mailman.interfaces.rules import IRule
+from zope.interface import implementer
diff --git a/src/mailman/rules/max_recipients.py b/src/mailman/rules/max_recipients.py
index 3b1d4f0c5..485368c0b 100644
--- a/src/mailman/rules/max_recipients.py
+++ b/src/mailman/rules/max_recipients.py
@@ -17,19 +17,15 @@
"""The maximum number of recipients rule."""
-from __future__ import absolute_import, print_function, unicode_literals
-
-__metaclass__ = type
__all__ = [
'MaximumRecipients',
]
from email.utils import getaddresses
-from zope.interface import implementer
-
from mailman.core.i18n import _
from mailman.interfaces.rules import IRule
+from zope.interface import implementer
diff --git a/src/mailman/rules/max_size.py b/src/mailman/rules/max_size.py
index 1e2b46184..4c8b58451 100644
--- a/src/mailman/rules/max_size.py
+++ b/src/mailman/rules/max_size.py
@@ -17,18 +17,14 @@
"""The maximum message size rule."""
-from __future__ import absolute_import, print_function, unicode_literals
-
-__metaclass__ = type
__all__ = [
'MaximumSize',
]
-from zope.interface import implementer
-
from mailman.core.i18n import _
from mailman.interfaces.rules import IRule
+from zope.interface import implementer
diff --git a/src/mailman/rules/moderation.py b/src/mailman/rules/moderation.py
index 46ed242fa..5b79677ed 100644
--- a/src/mailman/rules/moderation.py
+++ b/src/mailman/rules/moderation.py
@@ -17,23 +17,19 @@
"""Membership related rules."""
-from __future__ import absolute_import, print_function, unicode_literals
-
-__metaclass__ = type
__all__ = [
'MemberModeration',
'NonmemberModeration',
]
-from zope.component import getUtility
-from zope.interface import implementer
-
from mailman.core.i18n import _
from mailman.interfaces.action import Action
from mailman.interfaces.member import MemberRole
from mailman.interfaces.rules import IRule
from mailman.interfaces.usermanager import IUserManager
+from zope.component import getUtility
+from zope.interface import implementer
diff --git a/src/mailman/rules/news_moderation.py b/src/mailman/rules/news_moderation.py
index c4372eb80..358368624 100644
--- a/src/mailman/rules/news_moderation.py
+++ b/src/mailman/rules/news_moderation.py
@@ -17,19 +17,15 @@
"""The news moderation rule."""
-from __future__ import absolute_import, print_function, unicode_literals
-
-__metaclass__ = type
__all__ = [
'ModeratedNewsgroup',
]
-from zope.interface import implementer
-
from mailman.core.i18n import _
from mailman.interfaces.nntp import NewsgroupModeration
from mailman.interfaces.rules import IRule
+from zope.interface import implementer
diff --git a/src/mailman/rules/no_subject.py b/src/mailman/rules/no_subject.py
index 8f01f0c15..e66046832 100644
--- a/src/mailman/rules/no_subject.py
+++ b/src/mailman/rules/no_subject.py
@@ -17,18 +17,14 @@
"""The no-Subject header rule."""
-from __future__ import absolute_import, print_function, unicode_literals
-
-__metaclass__ = type
__all__ = [
'NoSubject',
]
-from zope.interface import implementer
-
from mailman.core.i18n import _
from mailman.interfaces.rules import IRule
+from zope.interface import implementer
diff --git a/src/mailman/rules/suspicious.py b/src/mailman/rules/suspicious.py
index 1841ed69e..fbd76b794 100644
--- a/src/mailman/rules/suspicious.py
+++ b/src/mailman/rules/suspicious.py
@@ -17,9 +17,6 @@
"""The historical 'suspicious header' rule."""
-from __future__ import absolute_import, print_function, unicode_literals
-
-__metaclass__ = type
__all__ = [
'SuspiciousHeader',
]
@@ -28,10 +25,10 @@ __all__ = [
import re
import logging
-from zope.interface import implementer
-
from mailman.core.i18n import _
from mailman.interfaces.rules import IRule
+from zope.interface import implementer
+
log = logging.getLogger('mailman.error')
diff --git a/src/mailman/rules/tests/test_approved.py b/src/mailman/rules/tests/test_approved.py
index 9976d4eff..83088da55 100644
--- a/src/mailman/rules/tests/test_approved.py
+++ b/src/mailman/rules/tests/test_approved.py
@@ -17,9 +17,6 @@
"""Test the `approved` handler."""
-from __future__ import absolute_import, print_function, unicode_literals
-
-__metaclass__ = type
__all__ = [
'TestApproved',
'TestApprovedNonASCII',
@@ -36,8 +33,7 @@ from mailman.app.lifecycle import create_list
from mailman.config import config
from mailman.rules import approved
from mailman.testing.helpers import (
- configuration,
- specialized_message_from_string as mfs)
+ configuration, specialized_message_from_string as mfs)
from mailman.testing.layers import ConfigLayer
@@ -433,7 +429,7 @@ class TestPasswordHashMigration(unittest.TestCase):
# hash is chosen after the original password is set. As long as the
# old password still validates, the migration happens automatically.
self._mlist.moderator_password = config.password_context.encrypt(
- b'super secret')
+ 'super secret')
self._rule = approved.Approved()
self._msg = mfs("""\
From: anne@example.com
@@ -450,7 +446,7 @@ A message body.
# hashing algorithm. When the old password is validated, it will be
# automatically migrated to the new hash.
self.assertEqual(self._mlist.moderator_password,
- b'{plaintext}super secret')
+ '{plaintext}super secret')
config_file = os.path.join(config.VAR_DIR, 'passlib.config')
# XXX passlib seems to choose the default hashing scheme even if it is
# deprecated. The default scheme is either specified explicitly, or
@@ -466,14 +462,14 @@ deprecated = roundup_plaintext
self._msg['Approved'] = 'super secret'
result = self._rule.check(self._mlist, self._msg, {})
self.assertTrue(result)
- self.assertEqual(self._mlist.moderator_password, b'super secret')
+ self.assertEqual(self._mlist.moderator_password, 'super secret')
def test_invalid_password_does_not_migrate(self):
# Now that the moderator password is set, change the default password
# hashing algorithm. When the old password is invalid, it will not be
# automatically migrated to the new hash.
self.assertEqual(self._mlist.moderator_password,
- b'{plaintext}super secret')
+ '{plaintext}super secret')
config_file = os.path.join(config.VAR_DIR, 'passlib.config')
# XXX passlib seems to choose the default hashing scheme even if it is
# deprecated. The default scheme is either specified explicitly, or
@@ -490,9 +486,10 @@ deprecated = roundup_plaintext
result = self._rule.check(self._mlist, self._msg, {})
self.assertFalse(result)
self.assertEqual(self._mlist.moderator_password,
- b'{plaintext}super secret')
+ '{plaintext}super secret')
+
class TestApprovedNoTextPlainPart(unittest.TestCase):
"""Test the approved handler with HTML-only messages."""
diff --git a/src/mailman/rules/tests/test_moderation.py b/src/mailman/rules/tests/test_moderation.py
index c0c3cf417..2db4e53cc 100644
--- a/src/mailman/rules/tests/test_moderation.py
+++ b/src/mailman/rules/tests/test_moderation.py
@@ -17,9 +17,6 @@
"""Test the `member-moderation` and `nonmember-moderation` rules."""
-from __future__ import absolute_import, print_function, unicode_literals
-
-__metaclass__ = type
__all__ = [
'TestModeration',
]
diff --git a/src/mailman/rules/truth.py b/src/mailman/rules/truth.py
index d50b5eae4..0bf3345b7 100644
--- a/src/mailman/rules/truth.py
+++ b/src/mailman/rules/truth.py
@@ -17,18 +17,14 @@
"""A rule which always matches."""
-from __future__ import absolute_import, print_function, unicode_literals
-
-__metaclass__ = type
__all__ = [
'Truth',
]
-from zope.interface import implementer
-
from mailman.core.i18n import _
from mailman.interfaces.rules import IRule
+from zope.interface import implementer
diff --git a/src/mailman/runners/archive.py b/src/mailman/runners/archive.py
index b49f5c265..f81f9ee3e 100644
--- a/src/mailman/runners/archive.py
+++ b/src/mailman/runners/archive.py
@@ -17,9 +17,6 @@
"""Archive runner."""
-from __future__ import absolute_import, print_function, unicode_literals
-
-__metaclass__ = type
__all__ = [
'ArchiveRunner',
]
@@ -31,7 +28,6 @@ import logging
from email.utils import parsedate_tz, mktime_tz
from datetime import datetime
from lazr.config import as_timedelta
-
from mailman.config import config
from mailman.core.runner import Runner
from mailman.interfaces.archiver import ClobberDate
diff --git a/src/mailman/runners/bounce.py b/src/mailman/runners/bounce.py
index 9312a9158..3a85006fe 100644
--- a/src/mailman/runners/bounce.py
+++ b/src/mailman/runners/bounce.py
@@ -20,11 +20,10 @@
import logging
from flufl.bounce import all_failures, scan_message
-from zope.component import getUtility
-
from mailman.app.bounces import ProbeVERP, StandardVERP, maybe_forward
from mailman.core.runner import Runner
from mailman.interfaces.bounce import BounceContext, IBounceProcessor
+from zope.component import getUtility
COMMASPACE = ', '
@@ -33,7 +32,7 @@ log = logging.getLogger('mailman.bounce')
elog = logging.getLogger('mailman.error')
-
+
class BounceRunner(Runner):
"""The bounce runner."""
diff --git a/src/mailman/runners/command.py b/src/mailman/runners/command.py
index 3d91f663a..b0775c4f4 100644
--- a/src/mailman/runners/command.py
+++ b/src/mailman/runners/command.py
@@ -17,9 +17,6 @@
"""-request robot command runner."""
-from __future__ import absolute_import, print_function, unicode_literals
-
-__metaclass__ = type
__all__ = [
'CommandRunner',
'Results',
@@ -31,21 +28,20 @@ __all__ = [
# -owner.
import re
+import six
import logging
-from StringIO import StringIO
from email.errors import HeaderParseError
from email.header import decode_header, make_header
from email.iterators import typed_subpart_iterator
-from zope.component import getUtility
-from zope.interface import implementer
-
from mailman.config import config
from mailman.core.i18n import _
from mailman.core.runner import Runner
from mailman.email.message import UserNotification
from mailman.interfaces.command import ContinueProcessing, IEmailResults
from mailman.interfaces.languages import ILanguageManager
+from zope.component import getUtility
+from zope.interface import implementer
NL = '\n'
@@ -76,7 +72,7 @@ class CommandFinder:
# Extract the subject header and do RFC 2047 decoding.
raw_subject = msg.get('subject', '')
try:
- subject = unicode(make_header(decode_header(raw_subject)))
+ subject = str(make_header(decode_header(raw_subject)))
# Mail commands must be ASCII.
self.command_lines.append(subject.encode('us-ascii'))
except (HeaderParseError, UnicodeError, LookupError):
@@ -84,7 +80,7 @@ class CommandFinder:
# subject is a unicode object, convert it to ASCII ignoring all
# bogus characters. Otherwise, there's nothing in the subject
# that we can use.
- if isinstance(raw_subject, unicode):
+ if isinstance(raw_subject, six.text_type):
safe_subject = raw_subject.encode('us-ascii', 'ignore')
self.command_lines.append(safe_subject)
# Find the first text/plain part of the message.
@@ -98,9 +94,9 @@ class CommandFinder:
if part is None:
# There was no text/plain part to be found.
return
- body = part.get_payload(decode=True)
+ body = part.get_payload()
# text/plain parts better have string payloads.
- assert isinstance(body, basestring), 'Non-string decoded payload'
+ assert isinstance(body, six.string_types), 'Non-string decoded payload'
lines = body.splitlines()
# Use no more lines than specified
max_lines = int(config.mailman.email_commands_max_lines)
@@ -118,7 +114,7 @@ class CommandFinder:
# Ensure that all the parts are unicodes. Since we only accept
# ASCII commands and arguments, ignore anything else.
parts = [(part
- if isinstance(part, unicode)
+ if isinstance(part, six.text_type)
else part.decode('ascii', 'ignore'))
for part in parts]
yield parts
@@ -130,20 +126,20 @@ class Results:
"""The email command results."""
def __init__(self, charset='us-ascii'):
- self._output = StringIO()
+ self._output = six.StringIO()
self.charset = charset
print(_("""\
The results of your email command are provided below.
"""), file=self._output)
def write(self, text):
- if not isinstance(text, unicode):
+ if isinstance(text, bytes):
text = text.decode(self.charset, 'ignore')
self._output.write(text)
- def __unicode__(self):
+ def __str__(self):
value = self._output.getvalue()
- assert isinstance(value, unicode), 'Not a unicode: %r' % value
+ assert isinstance(value, six.text_type), 'Not a unicode: %r' % value
return value
@@ -207,12 +203,12 @@ class CommandRunner(Runner):
if status == ContinueProcessing.no:
break
# All done. Strip blank lines and send the response.
- lines = filter(None, (line.strip() for line in finder.command_lines))
+ lines = [line.strip() for line in finder.command_lines if line]
if len(lines) > 0:
print(_('\n- Unprocessed:'), file=results)
for line in lines:
print(line, file=results)
- lines = filter(None, (line.strip() for line in finder.ignored_lines))
+ lines = [line.strip() for line in finder.ignored_lines if line]
if len(lines) > 0:
print(_('\n- Ignored:'), file=results)
for line in lines:
@@ -231,7 +227,7 @@ class CommandRunner(Runner):
# Find a charset for the response body. Try the original message's
# charset first, then ascii, then latin-1 and finally falling back to
# utf-8.
- reply_body = unicode(results)
+ reply_body = str(results)
for charset in (results.charset, 'us-ascii', 'latin-1'):
try:
reply_body.encode(charset)
diff --git a/src/mailman/runners/digest.py b/src/mailman/runners/digest.py
index e62c14abf..52bfb8859 100644
--- a/src/mailman/runners/digest.py
+++ b/src/mailman/runners/digest.py
@@ -17,9 +17,6 @@
"""Digest runner."""
-from __future__ import absolute_import, print_function, unicode_literals
-
-__metaclass__ = type
__all__ = [
'DigestRunner',
]
@@ -28,15 +25,11 @@ __all__ = [
import re
import logging
-# cStringIO doesn't support unicode.
-from StringIO import StringIO
from copy import deepcopy
from email.header import Header
from email.mime.message import MIMEMessage
from email.mime.text import MIMEText
from email.utils import formatdate, getaddresses, make_msgid
-from urllib2 import URLError
-
from mailman.config import config
from mailman.core.i18n import _
from mailman.core.runner import Runner
@@ -46,6 +39,8 @@ from mailman.interfaces.member import DeliveryMode, DeliveryStatus
from mailman.utilities.i18n import make
from mailman.utilities.mailbox import Mailbox
from mailman.utilities.string import oneline, wrap
+from six.moves import cStringIO as StringIO
+from six.moves.urllib_error import URLError
log = logging.getLogger('mailman.error')
@@ -260,17 +255,16 @@ class RFC1153Digester(Digester):
# multipart message. In that case, just stringify it.
payload = msg.get_payload(decode=True)
if not payload:
- # Split using bytes so as not to turn the payload into unicode
- # strings due to unicode_literals above.
- payload = msg.as_string().split(b'\n\n', 1)[1]
- try:
- # Do the decoding inside the try/except so that if the charset
- # conversion fails, we'll just drop back to ascii.
- charset = msg.get_content_charset('us-ascii')
- payload = payload.decode(charset, 'replace')
- except (LookupError, TypeError):
- # Unknown or empty charset.
- payload = payload.decode('us-ascii', 'replace')
+ payload = msg.as_string().split('\n\n', 1)[1]
+ if isinstance(payload, bytes):
+ try:
+ # Do the decoding inside the try/except so that if the charset
+ # conversion fails, we'll just drop back to ascii.
+ charset = msg.get_content_charset('us-ascii')
+ payload = payload.decode(charset, 'replace')
+ except (LookupError, TypeError):
+ # Unknown or empty charset.
+ payload = payload.decode('us-ascii', 'replace')
print(payload, file=self._text)
if not payload.endswith('\n'):
print(file=self._text)
@@ -384,9 +378,9 @@ class DigestRunner(Runner):
queue = config.switchboards['virgin']
queue.enqueue(mime,
recipients=mime_recipients,
- listname=mlist.fqdn_listname,
+ listid=mlist.list_id,
isdigest=True)
queue.enqueue(rfc1153,
recipients=rfc1153_recipients,
- listname=mlist.fqdn_listname,
+ listid=mlist.list_id,
isdigest=True)
diff --git a/src/mailman/runners/docs/command.rst b/src/mailman/runners/docs/command.rst
index a7a4da8ed..82ee33fbc 100644
--- a/src/mailman/runners/docs/command.rst
+++ b/src/mailman/runners/docs/command.rst
@@ -27,7 +27,7 @@ the sender. The command can be in the ``Subject`` header.
... """)
>>> from mailman.app.inject import inject_message
- >>> inject_message(mlist, msg, switchboard='command')
+ >>> filebase = inject_message(mlist, msg, switchboard='command')
>>> from mailman.runners.command import CommandRunner
>>> from mailman.testing.helpers import make_testable_runner
>>> command = make_testable_runner(CommandRunner)
@@ -63,9 +63,9 @@ And now the response is in the ``virgin`` queue.
>>> dump_msgdata(messages[0].msgdata)
_parsemsg : False
- listname : test@example.com
+ listid : test.example.com
nodecorate : True
- recipients : set([u'aperson@example.com'])
+ recipients : {'aperson@example.com'}
reduced_list_headers: True
version : ...
@@ -85,7 +85,7 @@ message is plain text.
... echo foo bar
... """)
- >>> inject_message(mlist, msg, switchboard='command')
+ >>> filebase = inject_message(mlist, msg, switchboard='command')
>>> command.run()
>>> messages = get_queue_messages('virgin')
>>> len(messages)
@@ -133,7 +133,8 @@ address, and the other is the results of his email command.
...
... """)
- >>> inject_message(mlist, msg, switchboard='command', subaddress='join')
+ >>> filebase = inject_message(
+ ... mlist, msg, switchboard='command', subaddress='join')
>>> command.run()
>>> messages = get_queue_messages('virgin', sort_on='subject')
>>> len(messages)
@@ -165,7 +166,8 @@ Similarly, to leave a mailing list, the user need only email the ``-leave`` or
...
... """)
- >>> inject_message(mlist, msg, switchboard='command', subaddress='leave')
+ >>> filebase = inject_message(
+ ... mlist, msg, switchboard='command', subaddress='leave')
>>> command.run()
>>> messages = get_queue_messages('virgin')
>>> len(messages)
@@ -200,7 +202,8 @@ The ``-confirm`` address is also available as an implicit command.
...
... """)
- >>> inject_message(mlist, msg, switchboard='command', subaddress='confirm')
+ >>> filebase = inject_message(
+ ... mlist, msg, switchboard='command', subaddress='confirm')
>>> command.run()
>>> messages = get_queue_messages('virgin')
>>> len(messages)
@@ -244,7 +247,7 @@ looked at by the command queue.
... echo baz qux
... """)
- >>> inject_message(mlist, msg, switchboard='command')
+ >>> filebase = inject_message(mlist, msg, switchboard='command')
>>> command.run()
>>> messages = get_queue_messages('virgin')
>>> len(messages)
@@ -276,7 +279,7 @@ The ``stop`` command is an alias for ``end``.
... echo baz qux
... """)
- >>> inject_message(mlist, msg, switchboard='command')
+ >>> filebase = inject_message(mlist, msg, switchboard='command')
>>> command.run()
>>> messages = get_queue_messages('virgin')
>>> len(messages)
diff --git a/src/mailman/runners/docs/digester.rst b/src/mailman/runners/docs/digester.rst
index cd0fba67c..fc59954bc 100644
--- a/src/mailman/runners/docs/digester.rst
+++ b/src/mailman/runners/docs/digester.rst
@@ -57,10 +57,11 @@ But the message metadata has a reference to the digest file.
_parsemsg : False
digest_number: 1
digest_path : .../lists/test@example.com/digest.1.1.mmdf
- listname : test@example.com
+ listid : test.example.com
version : 3
volume : 1
+..
# Put the messages back in the queue for the runner to handle.
>>> filebase = digestq.enqueue(entry.msg, entry.msgdata)
@@ -281,205 +282,6 @@ The RFC 1153 contains the digest in a single plain text message.
<BLANKLINE>
-Internationalized digests
-=========================
-
-When messages come in with a content-type character set different than that of
-the list's preferred language, recipients will get an internationalized
-digest. French is not enabled by default site-wide, so enable that now.
-::
-
- # Simulate the site administrator setting the default server language to
- # French in the configuration file. Without this, the English template
- # will be found and the masthead won't be translated.
- >>> config.push('french', """
- ... [mailman]
- ... default_language: fr
- ... """)
-
- >>> mlist.preferred_language = 'fr'
- >>> msg = message_from_string("""\
- ... From: aperson@example.org
- ... To: test@example.com
- ... Subject: =?iso-2022-jp?b?GyRCMGxIVhsoQg==?=
- ... MIME-Version: 1.0
- ... Content-Type: text/plain; charset=iso-2022-jp
- ... Content-Transfer-Encoding: 7bit
- ...
- ... \x1b$B0lHV\x1b(B
- ... """)
-
-Set the digest threshold to zero so that the digests will be sent immediately.
-
- >>> mlist.digest_size_threshold = 0
- >>> process(mlist, msg, {})
-
-The marker message is sitting in the digest queue.
-
- >>> len(digestq.files)
- 1
- >>> entry = get_queue_messages('digest')[0]
- >>> dump_msgdata(entry.msgdata)
- _parsemsg : False
- digest_number: 2
- digest_path : .../lists/test@example.com/digest.1.2.mmdf
- listname : test@example.com
- version : 3
- volume : 1
-
-The digest runner runs a loop, placing the two digests into the virgin queue.
-
- # Put the messages back in the queue for the runner to handle.
- >>> filebase = digestq.enqueue(entry.msg, entry.msgdata)
- >>> runner.run()
- >>> messages = get_queue_messages('virgin')
- >>> len(messages)
- 2
-
-One of which is the MIME digest and the other of which is the RFC 1153 digest.
-
- >>> mime, rfc1153 = mime_rfc1153(messages)
-
-You can see that the digests contain a mix of French and Japanese.
-
- >>> print(mime.msg.as_string())
- Content-Type: multipart/mixed; boundary="===============...=="
- MIME-Version: 1.0
- From: test-request@example.com
- Subject: Groupe Test, Vol 1, Parution 2
- To: test@example.com
- Reply-To: test@example.com
- Date: ...
- Message-ID: ...
- <BLANKLINE>
- --===============...==
- Content-Type: text/plain; charset="iso-8859-1"
- MIME-Version: 1.0
- Content-Transfer-Encoding: quoted-printable
- Content-Description: Groupe Test, Vol 1, Parution 2
- <BLANKLINE>
- Envoyez vos messages pour la liste Test =E0
- test@example.com
- <BLANKLINE>
- Pour vous (d=E9s)abonner par le web, consultez
- http://lists.example.com/listinfo/test@example.com
- <BLANKLINE>
- ou, par courriel, envoyez un message avec =AB=A0help=A0=BB dans le corps ou
- dans le sujet =E0
- test-request@example.com
- <BLANKLINE>
- Vous pouvez contacter l'administrateur de la liste =E0 l'adresse
- test-owner@example.com
- <BLANKLINE>
- Si vous r=E9pondez, n'oubliez pas de changer l'objet du message afin
- qu'il soit plus sp=E9cifique que =AB=A0Re: Contenu du groupe de Test...=A0=
- =BB
- --===============...==
- Content-Type: text/plain; charset="utf-8"
- MIME-Version: 1.0
- Content-Transfer-Encoding: base64
- Content-Description: Today's Topics (1 messages)
- <BLANKLINE>
- VGjDqG1lcyBkdSBqb3VyIDoKCiAgIDEuIOS4gOeVqiAoYXBlcnNvbkBleGFtcGxlLm9yZykK
- <BLANKLINE>
- --===============...==
- Content-Type: message/rfc822
- MIME-Version: 1.0
- <BLANKLINE>
- From: aperson@example.org
- To: test@example.com
- Subject: =?iso-2022-jp?b?GyRCMGxIVhsoQg==?=
- MIME-Version: 1.0
- Content-Type: text/plain; charset=iso-2022-jp
- Content-Transfer-Encoding: 7bit
- <BLANKLINE>
- $B0lHV(B
- <BLANKLINE>
- --===============...==
- Content-Type: text/plain; charset="iso-8859-1"
- MIME-Version: 1.0
- Content-Transfer-Encoding: quoted-printable
- Content-Description: =?utf-8?q?Pied_de_page_des_remises_group=C3=A9es?=
- <BLANKLINE>
- _______________________________________________
- Test mailing list
- test@example.com
- http://lists.example.com/listinfo/test@example.com
- <BLANKLINE>
- --===============...==--
-
-The RFC 1153 digest will be encoded in UTF-8 since it contains a mixture of
-French and Japanese characters.
-
- >>> print(rfc1153.msg.as_string())
- From: test-request@example.com
- Subject: Groupe Test, Vol 1, Parution 2
- To: test@example.com
- Reply-To: test@example.com
- Date: ...
- Message-ID: ...
- MIME-Version: 1.0
- Content-Type: text/plain; charset="utf-8"
- Content-Transfer-Encoding: base64
- <BLANKLINE>
- RW52b...
- <BLANKLINE>
-
-The content can be decoded to see the actual digest text.
-::
-
- # We must display the repr of the decoded value because doctests cannot
- # handle the non-ascii characters.
- >>> [repr(line)
- ... for line in rfc1153.msg.get_payload(decode=True).splitlines()]
- ["'Envoyez vos messages pour la liste Test \\xc3\\xa0'",
- "'\\ttest@example.com'",
- "''",
- "'Pour vous (d\\xc3\\xa9s)abonner par le web, consultez'",
- "'\\thttp://lists.example.com/listinfo/test@example.com'",
- "''",
- "'ou, par courriel, envoyez un message avec \\xc2\\xab\\xc2\\xa0...
- "'dans le sujet \\xc3\\xa0'",
- "'\\ttest-request@example.com'",
- "''",
- '"Vous pouvez contacter l\'administrateur de la liste \\xc3\\xa0 ...
- "'\\ttest-owner@example.com'",
- "''",
- '"Si vous r\\xc3\\xa9pondez, n\'oubliez pas de changer l\'objet du ...
- '"qu\'il soit plus sp\\xc3\\xa9cifique que \\xc2\\xab\\xc2\\xa0Re: ...
- "''",
- "'Th\\xc3\\xa8mes du jour :'",
- "''",
- "' 1. \\xe4\\xb8\\x80\\xe7\\x95\\xaa (aperson@example.org)'",
- "''",
- "''",
- "'---------------------------------------------------------------------...
- "''",
- "'From: aperson@example.org'",
- "'Subject: \\xe4\\xb8\\x80\\xe7\\x95\\xaa'",
- "'To: test@example.com'",
- "'Content-Type: text/plain; charset=iso-2022-jp'",
- "''",
- "'\\xe4\\xb8\\x80\\xe7\\x95\\xaa'",
- "''",
- "'------------------------------'",
- "''",
- "'Subject: Pied de page des remises group\\xc3\\xa9es'",
- "''",
- "'_______________________________________________'",
- "'Test mailing list'",
- "'test@example.com'",
- "'http://lists.example.com/listinfo/test@example.com'",
- "''",
- "''",
- "'------------------------------'",
- "''",
- "'Fin de Groupe Test, Vol 1, Parution 2'",
- "'*************************************'"]
-
- >>> config.pop('french')
-
-
Digest delivery
===============
@@ -538,12 +340,12 @@ and the other is the RFC 1153 digest.
Only wperson and xperson get the MIME digests.
>>> sorted(mime.msgdata['recipients'])
- [u'wperson@example.com', u'xperson@example.com']
+ ['wperson@example.com', 'xperson@example.com']
Only yperson and zperson get the RFC 1153 digests.
>>> sorted(rfc1153.msgdata['recipients'])
- [u'yperson@example.com', u'zperson@example.com']
+ ['yperson@example.com', 'zperson@example.com']
Now uperson decides that they would like to start receiving digests too.
::
@@ -558,10 +360,10 @@ Now uperson decides that they would like to start receiving digests too.
>>> mime, rfc1153 = mime_rfc1153(messages)
>>> sorted(mime.msgdata['recipients'])
- [u'uperson@example.com', u'wperson@example.com', u'xperson@example.com']
+ ['uperson@example.com', 'wperson@example.com', 'xperson@example.com']
>>> sorted(rfc1153.msgdata['recipients'])
- [u'yperson@example.com', u'zperson@example.com']
+ ['yperson@example.com', 'zperson@example.com']
At this point, both uperson and wperson decide that they'd rather receive
regular deliveries instead of digests. uperson would like to get any last
@@ -581,10 +383,10 @@ as much and does not want to receive one last digest.
>>> messages = get_queue_messages('virgin')
>>> mime, rfc1153 = mime_rfc1153(messages)
>>> sorted(mime.msgdata['recipients'])
- [u'uperson@example.com', u'xperson@example.com']
+ ['uperson@example.com', 'xperson@example.com']
>>> sorted(rfc1153.msgdata['recipients'])
- [u'yperson@example.com', u'zperson@example.com']
+ ['yperson@example.com', 'zperson@example.com']
Since uperson has received their last digest, they will not get any more of
them.
@@ -599,7 +401,7 @@ them.
>>> mime, rfc1153 = mime_rfc1153(messages)
>>> sorted(mime.msgdata['recipients'])
- [u'xperson@example.com']
+ ['xperson@example.com']
>>> sorted(rfc1153.msgdata['recipients'])
- [u'yperson@example.com', u'zperson@example.com']
+ ['yperson@example.com', 'zperson@example.com']
diff --git a/src/mailman/runners/docs/incoming.rst b/src/mailman/runners/docs/incoming.rst
index 0ae3336ca..d4fb65c85 100644
--- a/src/mailman/runners/docs/incoming.rst
+++ b/src/mailman/runners/docs/incoming.rst
@@ -54,7 +54,7 @@ Inject the message into the incoming queue, similar to the way the upstream
mail server normally would.
>>> from mailman.app.inject import inject_message
- >>> inject_message(mlist, msg)
+ >>> filebase = inject_message(mlist, msg)
The incoming runner runs until it is empty.
@@ -103,7 +103,7 @@ that it will be accepted and forward to the pipeline queue.
Inject the message into the incoming queue and run until the queue is empty.
- >>> inject_message(mlist, msg)
+ >>> filebase = inject_message(mlist, msg)
>>> incoming.run()
There are no messages left in the incoming queue.
@@ -156,7 +156,7 @@ pipeline queue.
>>> from mailman.testing.helpers import event_subscribers
>>> with event_subscribers(on_chain):
- ... inject_message(mlist, msg)
+ ... filebase = inject_message(mlist, msg)
... incoming.run()
<mailman.interfaces.chain.HoldEvent ...>
<mailman.chains.hold.HoldChain ...>
@@ -191,7 +191,7 @@ new chain and set it as the mailing list's start chain.
>>> msg.replace_header('message-id', '<second>')
>>> with event_subscribers(on_chain):
- ... inject_message(mlist, msg)
+ ... filebase = inject_message(mlist, msg)
... incoming.run()
<mailman.interfaces.chain.DiscardEvent ...>
<mailman.chains.discard.DiscardChain ...>
@@ -220,7 +220,7 @@ just create a new chain that does.
>>> msg.replace_header('message-id', '<third>')
>>> with event_subscribers(on_chain):
- ... inject_message(mlist, msg)
+ ... filebase = inject_message(mlist, msg)
... incoming.run()
<mailman.interfaces.chain.RejectEvent ...>
<mailman.chains.reject.RejectChain ...>
diff --git a/src/mailman/runners/docs/lmtp.rst b/src/mailman/runners/docs/lmtp.rst
index c2227581f..45e8a3453 100644
--- a/src/mailman/runners/docs/lmtp.rst
+++ b/src/mailman/runners/docs/lmtp.rst
@@ -20,7 +20,7 @@ Let's start a testable LMTP runner.
It also helps to have a nice LMTP client.
>>> lmtp = helpers.get_lmtp_client()
- (220, '... Python LMTP runner 1.0')
+ (220, b'... Python LMTP runner 1.0')
>>> lmtp.lhlo('remote.example.org')
(250, ...)
@@ -28,24 +28,8 @@ It also helps to have a nice LMTP client.
Posting address
===============
-If the mail server tries to send a message to a nonexistent mailing list, it
-will get a 550 error.
-
- >>> lmtp.sendmail(
- ... 'anne.person@example.com',
- ... ['mylist@example.com'], """\
- ... From: anne.person@example.com
- ... To: mylist@example.com
- ... Subject: An interesting message
- ... Message-ID: <aardvark>
- ...
- ... This is an interesting message.
- ... """)
- Traceback (most recent call last):
- ...
- SMTPDataError: (550, 'Requested action not taken: mailbox unavailable')
-
-Once the mailing list is created, the posting address is valid.
+Once the mailing list is created, the posting address is valid, and messages
+can be sent to the list.
::
>>> create_list('mylist@example.com')
@@ -82,7 +66,7 @@ queue.
This is an interesting message.
>>> dump_msgdata(messages[0].msgdata)
_parsemsg : False
- listname : mylist@example.com
+ listid : mylist.example.com
original_size: ...
to_list : True
version : ...
@@ -92,24 +76,8 @@ Sub-addresses
=============
The LMTP server understands each of the list's sub-addreses, such as `-join`,
-`-leave`, `-request` and so on. If the message is posted to an invalid
-sub-address though, it is rejected.
-
- >>> lmtp.sendmail(
- ... 'anne.person@example.com',
- ... ['mylist-bogus@example.com'], """\
- ... From: anne.person@example.com
- ... To: mylist-bogus@example.com
- ... Subject: Help
- ... Message-ID: <cow>
- ...
- ... Please help me.
- ... """)
- Traceback (most recent call last):
- ...
- SMTPDataError: (550, 'Requested action not taken: mailbox unavailable')
-
-But the message is accepted if posted to a valid sub-address.
+`-leave`, `-request` and so on. The message is accepted if posted to a valid
+sub-address.
>>> lmtp.sendmail(
... 'anne.person@example.com',
@@ -145,7 +113,7 @@ command queue for processing.
Please help me.
>>> dump_msgdata(messages[0].msgdata)
_parsemsg : False
- listname : mylist@example.com
+ listid : mylist.example.com
original_size: ...
subaddress : request
version : ...
@@ -172,7 +140,7 @@ A message to the `-bounces` address goes to the bounce processor.
1
>>> dump_msgdata(messages[0].msgdata)
_parsemsg : False
- listname : mylist@example.com
+ listid : mylist.example.com
original_size: ...
subaddress : bounces
version : ...
@@ -199,7 +167,7 @@ Confirmation messages go to the command processor...
1
>>> dump_msgdata(messages[0].msgdata)
_parsemsg : False
- listname : mylist@example.com
+ listid : mylist.example.com
original_size: ...
subaddress : confirm
version : ...
@@ -221,7 +189,7 @@ Confirmation messages go to the command processor...
1
>>> dump_msgdata(messages[0].msgdata)
_parsemsg : False
- listname : mylist@example.com
+ listid : mylist.example.com
original_size: ...
subaddress : join
version : ...
@@ -240,7 +208,7 @@ Confirmation messages go to the command processor...
1
>>> dump_msgdata(messages[0].msgdata)
_parsemsg : False
- listname : mylist@example.com
+ listid : mylist.example.com
original_size: ...
subaddress : join
version : ...
@@ -262,7 +230,7 @@ Confirmation messages go to the command processor...
1
>>> dump_msgdata(messages[0].msgdata)
_parsemsg : False
- listname : mylist@example.com
+ listid : mylist.example.com
original_size: ...
subaddress : leave
version : ...
@@ -281,7 +249,7 @@ Confirmation messages go to the command processor...
1
>>> dump_msgdata(messages[0].msgdata)
_parsemsg : False
- listname : mylist@example.com
+ listid : mylist.example.com
original_size: ...
subaddress : leave
version : ...
@@ -307,7 +275,7 @@ Messages to the `-owner` address also go to the incoming processor.
>>> dump_msgdata(messages[0].msgdata)
_parsemsg : False
envsender : noreply@example.com
- listname : mylist@example.com
+ listid : mylist.example.com
original_size: ...
subaddress : owner
to_owner : True
diff --git a/src/mailman/runners/docs/nntp.rst b/src/mailman/runners/docs/nntp.rst
index 372fa5744..4bd73cbab 100644
--- a/src/mailman/runners/docs/nntp.rst
+++ b/src/mailman/runners/docs/nntp.rst
@@ -37,7 +37,7 @@ are prohibited by NNTP servers such as INN.
The message gets copied to the NNTP queue for preparation and posting.
>>> filebase = config.switchboards['nntp'].enqueue(
- ... msg, listname='test@example.com')
+ ... msg, listid='test.example.com')
>>> from mailman.testing.helpers import make_testable_runner
>>> from mailman.runners.nntp import NNTPRunner
>>> runner = make_testable_runner(NNTPRunner, 'nntp')
diff --git a/src/mailman/runners/docs/outgoing.rst b/src/mailman/runners/docs/outgoing.rst
index d4a20d497..7c3d1a989 100644
--- a/src/mailman/runners/docs/outgoing.rst
+++ b/src/mailman/runners/docs/outgoing.rst
@@ -57,7 +57,7 @@ destination mailing list name. Simulate that here too.
>>> ignore = outgoing_queue.enqueue(
... msg, msgdata,
... tolist=True,
- ... listname=mlist.fqdn_listname)
+ ... listid=mlist.list_id)
Running the outgoing runner processes the message, delivering it to the
upstream SMTP.
@@ -105,7 +105,7 @@ just one.
>>> ignore = outgoing_queue.enqueue(
... msg, msgdata,
- ... listname=mlist.fqdn_listname)
+ ... listid=mlist.list_id)
>>> outgoing.run()
>>> messages = list(smtpd.messages)
>>> len(messages)
@@ -147,7 +147,7 @@ A handler can force VERP by setting the ``verp`` key in the message metadata.
>>> ignore = outgoing_queue.enqueue(
... msg, msgdata,
... verp=True,
- ... listname=mlist.fqdn_listname)
+ ... listid=mlist.list_id)
>>> outgoing.run()
>>> messages = list(smtpd.messages)
>>> len(messages)
@@ -174,7 +174,7 @@ Again, we get three individual messages, with VERP'd ``Sender`` headers.
>>> ignore = outgoing_queue.enqueue(
... msg, msgdata,
- ... listname=mlist.fqdn_listname)
+ ... listid=mlist.list_id)
>>> outgoing.run()
>>> messages = list(smtpd.messages)
>>> len(messages)
@@ -215,7 +215,7 @@ VERP'd.
>>> ignore = outgoing_queue.enqueue(
... msg, msgdata,
- ... listname=mlist.fqdn_listname)
+ ... listid=mlist.list_id)
>>> outgoing.run()
>>> messages = list(smtpd.messages)
>>> len(messages)
@@ -235,7 +235,7 @@ The second message sent to the list is also not VERP'd.
>>> ignore = outgoing_queue.enqueue(
... msg, msgdata,
- ... listname=mlist.fqdn_listname)
+ ... listid=mlist.list_id)
>>> outgoing.run()
>>> messages = list(smtpd.messages)
>>> len(messages)
@@ -254,7 +254,7 @@ The third message though is VERP'd.
>>> ignore = outgoing_queue.enqueue(
... msg, msgdata,
- ... listname=mlist.fqdn_listname)
+ ... listid=mlist.list_id)
>>> outgoing.run()
>>> messages = list(smtpd.messages)
>>> len(messages)
@@ -274,7 +274,7 @@ The next one is back to bulk delivery.
>>> ignore = outgoing_queue.enqueue(
... msg, msgdata,
- ... listname=mlist.fqdn_listname)
+ ... listid=mlist.list_id)
>>> outgoing.run()
>>> messages = list(smtpd.messages)
>>> len(messages)
@@ -308,7 +308,7 @@ The first message is VERP'd.
>>> ignore = outgoing_queue.enqueue(
... msg, msgdata,
- ... listname=mlist.fqdn_listname)
+ ... listid=mlist.list_id)
>>> outgoing.run()
>>> messages = list(smtpd.messages)
>>> len(messages)
@@ -328,7 +328,7 @@ As is the second message.
>>> ignore = outgoing_queue.enqueue(
... msg, msgdata,
- ... listname=mlist.fqdn_listname)
+ ... listid=mlist.list_id)
>>> outgoing.run()
>>> messages = list(smtpd.messages)
>>> len(messages)
@@ -348,7 +348,7 @@ And the third message.
>>> ignore = outgoing_queue.enqueue(
... msg, msgdata,
- ... listname=mlist.fqdn_listname)
+ ... listid=mlist.list_id)
>>> outgoing.run()
>>> messages = list(smtpd.messages)
>>> len(messages)
@@ -387,7 +387,7 @@ Neither the first message...
>>> ignore = outgoing_queue.enqueue(
... msg, msgdata,
- ... listname=mlist.fqdn_listname)
+ ... listid=mlist.list_id)
>>> outgoing.run()
>>> messages = list(smtpd.messages)
>>> len(messages)
@@ -402,7 +402,7 @@ Neither the first message...
>>> ignore = outgoing_queue.enqueue(
... msg, msgdata,
- ... listname=mlist.fqdn_listname)
+ ... listid=mlist.list_id)
>>> outgoing.run()
>>> messages = list(smtpd.messages)
>>> len(messages)
diff --git a/src/mailman/runners/incoming.py b/src/mailman/runners/incoming.py
index d75469a5e..a5d8fbea3 100644
--- a/src/mailman/runners/incoming.py
+++ b/src/mailman/runners/incoming.py
@@ -26,21 +26,17 @@ prepared for delivery. Rejections, discards, and holds are processed
immediately.
"""
-from __future__ import absolute_import, print_function, unicode_literals
-
-__metaclass__ = type
__all__ = [
'IncomingRunner',
]
-from zope.component import getUtility
-
from mailman.core.chains import process
from mailman.core.runner import Runner
from mailman.database.transaction import transaction
from mailman.interfaces.address import ExistingAddressError
from mailman.interfaces.usermanager import IUserManager
+from zope.component import getUtility
diff --git a/src/mailman/runners/lmtp.py b/src/mailman/runners/lmtp.py
index 7560fd962..85730bb7d 100644
--- a/src/mailman/runners/lmtp.py
+++ b/src/mailman/runners/lmtp.py
@@ -34,9 +34,6 @@ so that the peer mail server can provide better diagnostics.
http://www.faqs.org/rfcs/rfc2033.html
"""
-from __future__ import absolute_import, print_function, unicode_literals
-
-__metaclass__ = type
__all__ = [
'LMTPRunner',
]
@@ -48,8 +45,6 @@ import logging
import asyncore
from email.utils import parseaddr
-from zope.component import getUtility
-
from mailman.config import config
from mailman.core.runner import Runner
from mailman.database.transaction import transactional
@@ -57,6 +52,7 @@ from mailman.email.message import Message
from mailman.interfaces.listmanager import IListManager
from mailman.utilities.datetime import now
from mailman.utilities.email import add_message_hash
+from zope.component import getUtility
elog = logging.getLogger('mailman.error')
@@ -91,15 +87,15 @@ SUBADDRESS_QUEUES = dict(
)
DASH = '-'
-CRLF = b'\r\n'
-ERR_451 = b'451 Requested action aborted: error in processing'
-ERR_501 = b'501 Message has defects'
-ERR_502 = b'502 Error: command HELO not implemented'
-ERR_550 = b'550 Requested action not taken: mailbox unavailable'
-ERR_550_MID = b'550 No Message-ID header provided'
+CRLF = '\r\n'
+ERR_451 = '451 Requested action aborted: error in processing'
+ERR_501 = '501 Message has defects'
+ERR_502 = '502 Error: command HELO not implemented'
+ERR_550 = '550 Requested action not taken: mailbox unavailable'
+ERR_550_MID = '550 No Message-ID header provided'
# XXX Blech
-smtpd.__version__ = b'Python LMTP runner 1.0'
+smtpd.__version__ = 'Python LMTP runner 1.0'
@@ -147,6 +143,10 @@ class Channel(smtpd.SMTPChannel):
"""HELO is not a valid LMTP command."""
self.push(ERR_502)
+ ## def push(self, arg):
+ ## import pdb; pdb.set_trace()
+ ## return super().push(arg)
+
class LMTPRunner(Runner, smtpd.SMTPServer):
@@ -202,18 +202,19 @@ class LMTPRunner(Runner, smtpd.SMTPServer):
for to in rcpttos:
try:
to = parseaddr(to)[1].lower()
- listname, subaddress, domain = split_recipient(to)
+ local, subaddress, domain = split_recipient(to)
slog.debug('%s to: %s, list: %s, sub: %s, dom: %s',
- message_id, to, listname, subaddress, domain)
- listname += '@' + domain
+ message_id, to, local, subaddress, domain)
+ listname = '{}@{}'.format(local, domain)
if listname not in listnames:
status.append(ERR_550)
continue
+ listid = '{}.{}'.format(local, domain)
# The recipient is a valid mailing list. Find the subaddress
# if there is one, and set things up to enqueue to the proper
# queue.
queue = None
- msgdata = dict(listname=listname,
+ msgdata = dict(listid=listid,
original_size=msg.original_size,
received_time=received_time)
canonical_subaddress = SUBADDRESS_NAMES.get(subaddress)
@@ -243,7 +244,7 @@ class LMTPRunner(Runner, smtpd.SMTPServer):
config.switchboards[queue].enqueue(msg, msgdata)
slog.debug('%s subaddress: %s, queue: %s',
message_id, canonical_subaddress, queue)
- status.append(b'250 Ok')
+ status.append('250 Ok')
except Exception:
slog.exception('Queue detection: %s', msg['message-id'])
config.db.abort()
diff --git a/src/mailman/runners/nntp.py b/src/mailman/runners/nntp.py
index 493f8d09a..7fb16f1b2 100644
--- a/src/mailman/runners/nntp.py
+++ b/src/mailman/runners/nntp.py
@@ -17,9 +17,6 @@
"""NNTP runner."""
-from __future__ import absolute_import, print_function, unicode_literals
-
-__metaclass__ = type
__all__ = [
'NNTPRunner',
]
@@ -31,11 +28,11 @@ import socket
import logging
import nntplib
-from cStringIO import StringIO
-
from mailman.config import config
from mailman.core.runner import Runner
from mailman.interfaces.nntp import NewsgroupModeration
+from six.moves import cStringIO as StringIO
+
COMMA = ','
COMMASPACE = ', '
@@ -82,7 +79,7 @@ class NNTPRunner(Runner):
user=config.nntp.user,
password=config.nntp.password)
conn.post(fp)
- except nntplib.error_temp:
+ except nntplib.NNTPTemporaryError:
log.exception('{0} NNTP error for {1}'.format(
msg.get('message-id', 'n/a'), mlist.fqdn_listname))
except socket.error:
@@ -111,9 +108,9 @@ def prepare_message(mlist, msg, msgdata):
del msg['approved']
msg['Approved'] = mlist.posting_address
# Should we restore the original, non-prefixed subject for gatewayed
- # messages? TK: We use stripped_subject (prefix stripped) which was
- # crafted in CookHeaders.py to ensure prefix was stripped from the subject
- # came from mailing list user.
+ # messages? TK: We use stripped_subject (prefix stripped) which was crafted
+ # in the subject-prefix handler to ensure prefix was stripped from the
+ # subject came from mailing list user.
stripped_subject = msgdata.get('stripped_subject',
msgdata.get('original_subject'))
if not mlist.nntp_prefix_subject_too and stripped_subject is not None:
diff --git a/src/mailman/runners/outgoing.py b/src/mailman/runners/outgoing.py
index db0d847c4..9af4e7c11 100644
--- a/src/mailman/runners/outgoing.py
+++ b/src/mailman/runners/outgoing.py
@@ -17,14 +17,16 @@
"""Outgoing runner."""
+__all__ = [
+ 'OutgoingRunner',
+ ]
+
+
import socket
import logging
from datetime import datetime
from lazr.config import as_boolean, as_timedelta
-from uuid import UUID
-from zope.component import getUtility
-
from mailman.config import config
from mailman.core.runner import Runner
from mailman.interfaces.bounce import BounceContext, IBounceProcessor
@@ -34,6 +36,8 @@ from mailman.interfaces.pending import IPendings
from mailman.interfaces.subscriptions import ISubscriptionService
from mailman.utilities.datetime import now
from mailman.utilities.modules import find_name
+from uuid import UUID
+from zope.component import getUtility
# This controls how often _do_periodic() will try to deal with deferred
diff --git a/src/mailman/runners/pipeline.py b/src/mailman/runners/pipeline.py
index 13226c6fc..357863d2e 100644
--- a/src/mailman/runners/pipeline.py
+++ b/src/mailman/runners/pipeline.py
@@ -22,6 +22,11 @@ through the 'preparation pipeline'. This pipeline adds, deletes and modifies
headers, calculates message recipients, and more.
"""
+__all__ = [
+ 'PipelineRunner',
+ ]
+
+
from mailman.core.pipelines import process
from mailman.core.runner import Runner
diff --git a/src/mailman/runners/rest.py b/src/mailman/runners/rest.py
index 5980e6263..d39a8a6ff 100644
--- a/src/mailman/runners/rest.py
+++ b/src/mailman/runners/rest.py
@@ -17,9 +17,6 @@
"""Start the administrative HTTP server."""
-from __future__ import absolute_import, print_function, unicode_literals
-
-__metaclass__ = type
__all__ = [
'RESTRunner',
]
diff --git a/src/mailman/runners/retry.py b/src/mailman/runners/retry.py
index b4148ee3a..f4705ba75 100644
--- a/src/mailman/runners/retry.py
+++ b/src/mailman/runners/retry.py
@@ -17,9 +17,6 @@
"""Retry delivery."""
-from __future__ import absolute_import, print_function, unicode_literals
-
-__metaclass__ = type
__all__ = [
'RetryRunner',
]
diff --git a/src/mailman/runners/tests/test_archiver.py b/src/mailman/runners/tests/test_archiver.py
index e11b6c805..9e3d9626c 100644
--- a/src/mailman/runners/tests/test_archiver.py
+++ b/src/mailman/runners/tests/test_archiver.py
@@ -17,9 +17,6 @@
"""Test the archive runner."""
-from __future__ import absolute_import, print_function, unicode_literals
-
-__metaclass__ = type
__all__ = [
'TestArchiveRunner',
]
@@ -29,19 +26,17 @@ import os
import unittest
from email import message_from_file
-from zope.interface import implementer
-
from mailman.app.lifecycle import create_list
from mailman.config import config
from mailman.interfaces.archiver import IArchiver
from mailman.interfaces.mailinglist import IListArchiverSet
from mailman.runners.archive import ArchiveRunner
from mailman.testing.helpers import (
- configuration,
- make_testable_runner,
+ configuration, make_testable_runner,
specialized_message_from_string as mfs)
from mailman.testing.layers import ConfigLayer
from mailman.utilities.datetime import RFC822_DATE_FMT, factory, now
+from zope.interface import implementer
@@ -110,7 +105,7 @@ First post!
# Ensure that the archive runner ends up archiving the message.
self._archiveq.enqueue(
self._msg, {},
- listname=self._mlist.fqdn_listname,
+ listid=self._mlist.list_id,
received_time=now())
self._runner.run()
# There should now be a copy of the message in the file system.
@@ -126,7 +121,7 @@ First post!
self._msg['Date'] = now(strip_tzinfo=False).strftime(RFC822_DATE_FMT)
self._archiveq.enqueue(
self._msg, {},
- listname=self._mlist.fqdn_listname,
+ listid=self._mlist.list_id,
received_time=now())
self._runner.run()
# There should now be a copy of the message in the file system.
@@ -144,7 +139,7 @@ First post!
self._msg['Date'] = now(strip_tzinfo=False).strftime(RFC822_DATE_FMT)
self._archiveq.enqueue(
self._msg, {},
- listname=self._mlist.fqdn_listname,
+ listid=self._mlist.list_id,
received_time=now())
self._runner.run()
# There should now be a copy of the message in the file system.
@@ -163,7 +158,7 @@ First post!
# again), fast forward a few days.
self._archiveq.enqueue(
self._msg, {},
- listname=self._mlist.fqdn_listname,
+ listid=self._mlist.list_id,
received_time=now(strip_tzinfo=False))
self._runner.run()
# There should now be a copy of the message in the file system.
@@ -182,7 +177,7 @@ First post!
# again as will happen in the runner), fast forward a few days.
self._archiveq.enqueue(
self._msg, {},
- listname=self._mlist.fqdn_listname)
+ listid=self._mlist.list_id)
factory.fast_forward(days=4)
self._runner.run()
# There should now be a copy of the message in the file system.
@@ -205,7 +200,7 @@ First post!
# again as will happen in the runner), fast forward a few days.
self._archiveq.enqueue(
self._msg, {},
- listname=self._mlist.fqdn_listname)
+ listid=self._mlist.list_id)
factory.fast_forward(days=4)
self._runner.run()
# There should now be a copy of the message in the file system.
@@ -228,7 +223,7 @@ First post!
# again as will happen in the runner), fast forward a few days.
self._archiveq.enqueue(
self._msg, {},
- listname=self._mlist.fqdn_listname)
+ listid=self._mlist.list_id)
factory.fast_forward(days=4)
self._runner.run()
# There should now be a copy of the message in the file system.
@@ -249,6 +244,6 @@ First post!
config.db.store.commit()
self._archiveq.enqueue(
self._msg, {},
- listname=self._mlist.fqdn_listname)
+ listid=self._mlist.list_id)
self._runner.run()
self.assertEqual(os.listdir(config.MESSAGES_DIR), [])
diff --git a/src/mailman/runners/tests/test_bounce.py b/src/mailman/runners/tests/test_bounce.py
index 315a81c22..875437dc2 100644
--- a/src/mailman/runners/tests/test_bounce.py
+++ b/src/mailman/runners/tests/test_bounce.py
@@ -17,9 +17,6 @@
"""Test the bounce runner."""
-from __future__ import absolute_import, print_function, unicode_literals
-
-__metaclass__ = type
__all__ = [
'TestBounceRunner',
'TestBounceRunnerBug876774',
@@ -29,9 +26,6 @@ __all__ = [
import unittest
-from zope.component import getUtility
-from zope.interface import implementer
-
from mailman.app.bounces import send_probe
from mailman.app.lifecycle import create_list
from mailman.config import config
@@ -42,11 +36,11 @@ from mailman.interfaces.styles import IStyle, IStyleManager
from mailman.interfaces.usermanager import IUserManager
from mailman.runners.bounce import BounceRunner
from mailman.testing.helpers import (
- LogFileMark,
- get_queue_messages,
- make_testable_runner,
+ LogFileMark, get_queue_messages, make_testable_runner,
specialized_message_from_string as message_from_string)
from mailman.testing.layers import ConfigLayer
+from zope.component import getUtility
+from zope.interface import implementer
@@ -69,7 +63,7 @@ To: test-bounces+anne=example.com@example.com
Message-Id: <first>
""")
- self._msgdata = dict(listname='test@example.com')
+ self._msgdata = dict(listid='test.example.com')
self._processor = getUtility(IBounceProcessor)
config.push('site owner', """
[mailman]
@@ -284,7 +278,7 @@ To: test-bounces+anne=example.com@example.com
Message-Id: <first>
""")
- self._bounceq.enqueue(bounce, dict(listname='test@example.com'))
+ self._bounceq.enqueue(bounce, dict(listid='test.example.com'))
self.assertEqual(len(self._bounceq.files), 1)
self._runner.run()
self.assertEqual(len(get_queue_messages('bounces')), 0)
diff --git a/src/mailman/runners/tests/test_confirm.py b/src/mailman/runners/tests/test_confirm.py
index 40fae368f..11514044a 100644
--- a/src/mailman/runners/tests/test_confirm.py
+++ b/src/mailman/runners/tests/test_confirm.py
@@ -17,9 +17,6 @@
"""Test the `confirm` command."""
-from __future__ import absolute_import, print_function, unicode_literals
-
-__metaclass__ = type
__all__ = [
'TestConfirm',
]
@@ -29,8 +26,6 @@ import unittest
from datetime import datetime
from email.iterators import body_line_iterator
-from zope.component import getUtility
-
from mailman.app.lifecycle import create_list
from mailman.config import config
from mailman.database.transaction import transaction
@@ -38,10 +33,10 @@ from mailman.interfaces.registrar import IRegistrar
from mailman.interfaces.usermanager import IUserManager
from mailman.runners.command import CommandRunner
from mailman.testing.helpers import (
- get_queue_messages,
- make_testable_runner,
+ get_queue_messages, make_testable_runner,
specialized_message_from_string as mfs)
from mailman.testing.layers import ConfigLayer
+from zope.component import getUtility
@@ -68,7 +63,7 @@ To: test-confirm@example.com
""")
msg['Subject'] = subject
- self._commandq.enqueue(msg, dict(listname='test@example.com'))
+ self._commandq.enqueue(msg, dict(listid='test.example.com'))
self._runner.run()
# Anne is now a confirmed member so her user record and email address
# should exist in the database.
@@ -88,7 +83,7 @@ To: test-confirm@example.com
""")
msg['Subject'] = subject
- self._commandq.enqueue(msg, dict(listname='test@example.com'))
+ self._commandq.enqueue(msg, dict(listid='test.example.com'))
self._runner.run()
# Anne is now a confirmed member so her user record and email address
# should exist in the database.
@@ -144,7 +139,7 @@ Franziskanerstra=C3=9Fe
""")
msg['Subject'] = subject
msg['To'] = to
- self._commandq.enqueue(msg, dict(listname='test@example.com'))
+ self._commandq.enqueue(msg, dict(listid='test.example.com'))
self._runner.run()
# Anne is now a confirmed member so her user record and email address
# should exist in the database.
@@ -177,7 +172,7 @@ Franziskanerstra=C3=9Fe
""")
msg['Subject'] = subject
msg['To'] = to
- self._commandq.enqueue(msg, dict(listname='test@example.com'))
+ self._commandq.enqueue(msg, dict(listid='test.example.com'))
self._runner.run()
# Anne is now a confirmed member so her user record and email address
# should exist in the database.
@@ -208,7 +203,7 @@ From: Anne Person <anne@example.org>
""")
msg['Subject'] = subject
msg['To'] = to
- self._commandq.enqueue(msg, dict(listname='test@example.com',
+ self._commandq.enqueue(msg, dict(listid='test.example.com',
subaddress='confirm'))
self._runner.run()
# Anne is now a confirmed member so her user record and email address
@@ -223,7 +218,7 @@ From: Anne Person <anne@example.org>
# one 'Confirmation email' line.
confirmation_lines = []
in_results = False
- for line in body_line_iterator(messages[0].msg, decode=True):
+ for line in body_line_iterator(messages[0].msg):
line = line.strip()
if in_results:
if line.startswith('- Done'):
@@ -253,7 +248,7 @@ From: Anne Person <anne@example.org>
""")
msg['Subject'] = subject
msg['To'] = to
- self._commandq.enqueue(msg, dict(listname='test@example.com',
+ self._commandq.enqueue(msg, dict(listid='test.example.com',
subaddress='confirm'))
self._runner.run()
# Now there's a email command notification and a welcome message. All
diff --git a/src/mailman/runners/tests/test_digest.py b/src/mailman/runners/tests/test_digest.py
index fb1bb7071..83156f04e 100644
--- a/src/mailman/runners/tests/test_digest.py
+++ b/src/mailman/runners/tests/test_digest.py
@@ -17,26 +17,25 @@
"""Test the digest runner."""
-from __future__ import absolute_import, print_function, unicode_literals
-
-__metaclass__ = type
__all__ = [
'TestDigest',
+ 'TestI18nDigest',
]
import unittest
-from StringIO import StringIO
from email.iterators import _structure as structure
from email.mime.text import MIMEText
+from io import StringIO
from mailman.app.lifecycle import create_list
from mailman.config import config
from mailman.email.message import Message
from mailman.runners.digest import DigestRunner
from mailman.testing.helpers import (
LogFileMark, digest_mbox, get_queue_messages, make_digest_messages,
- make_testable_runner, message_from_string)
+ make_testable_runner, message_from_string,
+ specialized_message_from_string as mfs)
from mailman.testing.layers import ConfigLayer
from string import Template
@@ -140,3 +139,77 @@ multipart/mixed
text/plain
text/plain
""")
+
+
+
+class TestI18nDigest(unittest.TestCase):
+ layer = ConfigLayer
+ maxDiff = None
+
+ def setUp(self):
+ config.push('french', """
+ [mailman]
+ default_language: fr
+ """)
+ self.addCleanup(config.pop, 'french')
+ self._mlist = create_list('test@example.com')
+ self._mlist.preferred_language = 'fr'
+ self._mlist.digest_size_threshold = 0
+ self._process = config.handlers['to-digest'].process
+ self._runner = make_testable_runner(DigestRunner)
+
+ def test_multilingual_digest(self):
+ # When messages come in with a content-type character set different
+ # than that of the list's preferred language, recipients will get an
+ # internationalized digest.
+ msg = mfs("""\
+From: aperson@example.org
+To: test@example.com
+Subject: =?iso-2022-jp?b?GyRCMGxIVhsoQg==?=
+MIME-Version: 1.0
+Content-Type: text/plain; charset=iso-2022-jp
+Content-Transfer-Encoding: 7bit
+
+\x1b$B0lHV\x1b(B
+""")
+ self._process(self._mlist, msg, {})
+ self._runner.run()
+ # There are two digests in the virgin queue; one is the MIME digest
+ # and the other is the RFC 1153 digest.
+ messages = get_queue_messages('virgin')
+ self.assertEqual(len(messages), 2)
+ if messages[0].msg.is_multipart():
+ mime, rfc1153 = messages[0].msg, messages[1].msg
+ else:
+ rfc1153, mime = messages[0].msg, messages[1].msg
+ # The MIME version contains a mix of French and Japanese. The digest
+ # chrome added by Mailman is in French.
+ self.assertEqual(mime['subject'].encode(),
+ '=?iso-8859-1?q?Groupe_Test=2C_Vol_1=2C_Parution_1?=')
+ self.assertEqual(str(mime['subject']),
+ 'Groupe Test, Vol 1, Parution 1')
+ # The first subpart contains the iso-8859-1 masthead.
+ masthead = mime.get_payload(0).get_payload(decode=True).decode(
+ 'iso-8859-1')
+ self.assertMultiLineEqual(masthead.splitlines()[0],
+ 'Envoyez vos messages pour la liste Test à')
+ # The second subpart contains the utf-8 table of contents.
+ self.assertEqual(mime.get_payload(1)['content-description'],
+ "Today's Topics (1 messages)")
+ toc = mime.get_payload(1).get_payload(decode=True).decode('utf-8')
+ self.assertMultiLineEqual(toc.splitlines()[0], 'Thèmes du jour :')
+ # The third subpart contains the posted message in Japanese.
+ self.assertEqual(mime.get_payload(2).get_content_type(),
+ 'message/rfc822')
+ post = mime.get_payload(2).get_payload(0)
+ self.assertEqual(post['subject'], '=?iso-2022-jp?b?GyRCMGxIVhsoQg==?=')
+ # Compare the bytes so that this module doesn't contain string
+ # literals in multiple incompatible character sets.
+ self.assertEqual(post.get_payload(decode=True), b'\x1b$B0lHV\x1b(B\n')
+ # The RFC 1153 digest will have the same subject, but its payload will
+ # be recast into utf-8.
+ self.assertEqual(str(rfc1153['subject']),
+ 'Groupe Test, Vol 1, Parution 1')
+ self.assertEqual(rfc1153.get_charset(), 'utf-8')
+ lines = rfc1153.get_payload(decode=True).decode('utf-8').splitlines()
+ self.assertEqual(lines[0], 'Envoyez vos messages pour la liste Test à')
diff --git a/src/mailman/runners/tests/test_incoming.py b/src/mailman/runners/tests/test_incoming.py
index 9830fedb9..77fe2da02 100644
--- a/src/mailman/runners/tests/test_incoming.py
+++ b/src/mailman/runners/tests/test_incoming.py
@@ -17,9 +17,6 @@
"""Test the incoming queue runner."""
-from __future__ import absolute_import, print_function, unicode_literals
-
-__metaclass__ = type
__all__ = [
'TestIncoming',
]
@@ -32,8 +29,7 @@ from mailman.chains.base import TerminalChainBase
from mailman.config import config
from mailman.runners.incoming import IncomingRunner
from mailman.testing.helpers import (
- get_queue_messages,
- make_testable_runner,
+ get_queue_messages, make_testable_runner,
specialized_message_from_string as mfs)
from mailman.testing.layers import ConfigLayer
@@ -76,7 +72,7 @@ To: test@example.com
def test_posting(self):
# A message posted to the list goes through the posting chain.
- msgdata = dict(listname='test@example.com')
+ msgdata = dict(listid='test.example.com')
config.switchboards['in'].enqueue(self._msg, msgdata)
self._in.run()
messages = get_queue_messages('out')
@@ -85,7 +81,7 @@ To: test@example.com
def test_owner(self):
# A message posted to the list goes through the posting chain.
- msgdata = dict(listname='test@example.com',
+ msgdata = dict(listid='test.example.com',
to_owner=True)
config.switchboards['in'].enqueue(self._msg, msgdata)
self._in.run()
diff --git a/src/mailman/runners/tests/test_join.py b/src/mailman/runners/tests/test_join.py
index fbea9e661..df24bc06b 100644
--- a/src/mailman/runners/tests/test_join.py
+++ b/src/mailman/runners/tests/test_join.py
@@ -17,9 +17,6 @@
"""Test mailing list joins."""
-from __future__ import absolute_import, print_function, unicode_literals
-
-__metaclass__ = type
__all__ = [
'TestJoin',
'TestJoinWithDigests',
@@ -29,8 +26,6 @@ __all__ = [
import unittest
from email.iterators import body_line_iterator
-from zope.component import getUtility
-
from mailman.app.lifecycle import create_list
from mailman.config import config
from mailman.interfaces.member import DeliveryMode
@@ -42,6 +37,7 @@ from mailman.testing.helpers import (
get_queue_messages, make_testable_runner, reset_the_world,
specialized_message_from_string as mfs)
from mailman.testing.layers import ConfigLayer
+from zope.component import getUtility
@@ -72,7 +68,7 @@ subscribe
# Adding the subaddress to the metadata dictionary mimics what happens
# when the above email message is first processed by the lmtp runner.
# For convenience, we skip that step in this test.
- self._commandq.enqueue(msg, dict(listname='test@example.com',
+ self._commandq.enqueue(msg, dict(listid='test.example.com',
subaddress='join'))
self._runner.run()
# There will be two messages in the queue. The first one is a reply
@@ -87,7 +83,7 @@ subscribe
# one 'Confirmation email' line.
confirmation_lines = []
in_results = False
- for line in body_line_iterator(messages[0].msg, decode=True):
+ for line in body_line_iterator(messages[0].msg):
line = line.strip()
if in_results:
if line.startswith('- Done'):
@@ -112,7 +108,7 @@ To: test-join@example.com
Subject: join
""")
- self._commandq.enqueue(msg, dict(listname='test@example.com'))
+ self._commandq.enqueue(msg, dict(listid='test.example.com'))
self._runner.run()
# There will be one message in the queue - a reply to Anne notifying
# her of the status of her command email. Because Anne is already
@@ -125,7 +121,7 @@ Subject: join
# one 'Confirmation email' line.
confirmation_lines = []
in_results = False
- for line in body_line_iterator(messages[0].msg, decode=True):
+ for line in body_line_iterator(messages[0].msg):
line = line.strip()
if in_results:
if line.startswith('- Done'):
@@ -181,7 +177,7 @@ To: test-request@example.com
join
""")
- self._commandq.enqueue(msg, dict(listname='test@example.com'))
+ self._commandq.enqueue(msg, dict(listid='test.example.com'))
self._runner.run()
anne = self._confirm()
self.assertEqual(anne.address.email, 'anne@example.org')
@@ -195,7 +191,7 @@ To: test-request@example.com
join digest=no
""")
- self._commandq.enqueue(msg, dict(listname='test@example.com'))
+ self._commandq.enqueue(msg, dict(listid='test.example.com'))
self._runner.run()
anne = self._confirm()
self.assertEqual(anne.address.email, 'anne@example.org')
@@ -209,7 +205,7 @@ To: test-request@example.com
join digest=mime
""")
- self._commandq.enqueue(msg, dict(listname='test@example.com'))
+ self._commandq.enqueue(msg, dict(listid='test.example.com'))
self._runner.run()
anne = self._confirm()
self.assertEqual(anne.address.email, 'anne@example.org')
@@ -223,7 +219,7 @@ To: test-request@example.com
join digest=plain
""")
- self._commandq.enqueue(msg, dict(listname='test@example.com'))
+ self._commandq.enqueue(msg, dict(listid='test.example.com'))
self._runner.run()
anne = self._confirm()
self.assertEqual(anne.address.email, 'anne@example.org')
diff --git a/src/mailman/runners/tests/test_lmtp.py b/src/mailman/runners/tests/test_lmtp.py
index 26308548c..44b6a0889 100644
--- a/src/mailman/runners/tests/test_lmtp.py
+++ b/src/mailman/runners/tests/test_lmtp.py
@@ -17,9 +17,6 @@
"""Tests for the LMTP server."""
-from __future__ import absolute_import, print_function, unicode_literals
-
-__metaclass__ = type
__all__ = [
'TestLMTP',
]
@@ -30,7 +27,6 @@ import smtplib
import unittest
from datetime import datetime
-
from mailman.config import config
from mailman.app.lifecycle import create_list
from mailman.database.transaction import transaction
@@ -67,7 +63,7 @@ Subject: This has no Message-ID header
# reasons)
self.assertEqual(cm.exception.smtp_code, 550)
self.assertEqual(cm.exception.smtp_error,
- 'No Message-ID header provided')
+ b'No Message-ID header provided')
def test_message_id_hash_is_added(self):
self._lmtp.sendmail('anne@example.com', ['test@example.com'], """\
@@ -118,6 +114,36 @@ Message-ID: <ant>
queue_directory = os.path.join(config.QUEUE_DIR, 'lmtp')
self.assertFalse(os.path.isdir(queue_directory))
+ def test_nonexistent_mailing_list(self):
+ # Trying to post to a nonexistent mailing list is an error.
+ with self.assertRaises(smtplib.SMTPDataError) as cm:
+ self._lmtp.sendmail('anne@example.com',
+ ['notalist@example.com'], """\
+From: anne.person@example.com
+To: notalist@example.com
+Subject: An interesting message
+Message-ID: <aardvark>
+
+""")
+ self.assertEqual(cm.exception.smtp_code, 550)
+ self.assertEqual(cm.exception.smtp_error,
+ b'Requested action not taken: mailbox unavailable')
+
+ def test_missing_subaddress(self):
+ # Trying to send a message to a bogus subaddress is an error.
+ with self.assertRaises(smtplib.SMTPDataError) as cm:
+ self._lmtp.sendmail('anne@example.com',
+ ['test-bogus@example.com'], """\
+From: anne.person@example.com
+To: test-bogus@example.com
+Subject: An interesting message
+Message-ID: <aardvark>
+
+""")
+ self.assertEqual(cm.exception.smtp_code, 550)
+ self.assertEqual(cm.exception.smtp_error,
+ b'Requested action not taken: mailbox unavailable')
+
class TestBugs(unittest.TestCase):
@@ -142,5 +168,5 @@ Message-ID: <alpha>
""")
messages = get_queue_messages('in')
self.assertEqual(len(messages), 1)
- self.assertEqual(messages[0].msgdata['listname'],
- 'my-list@example.com')
+ self.assertEqual(messages[0].msgdata['listid'],
+ 'my-list.example.com')
diff --git a/src/mailman/runners/tests/test_nntp.py b/src/mailman/runners/tests/test_nntp.py
index 3570d1a6f..e3218af33 100644
--- a/src/mailman/runners/tests/test_nntp.py
+++ b/src/mailman/runners/tests/test_nntp.py
@@ -17,9 +17,6 @@
"""Test the NNTP runner and related utilities."""
-from __future__ import absolute_import, print_function, unicode_literals
-
-__metaclass__ = type
__all__ = [
'TestPrepareMessage',
'TestNNTPRunner',
@@ -36,10 +33,7 @@ from mailman.config import config
from mailman.interfaces.nntp import NewsgroupModeration
from mailman.runners import nntp
from mailman.testing.helpers import (
- LogFileMark,
- configuration,
- get_queue_messages,
- make_testable_runner,
+ LogFileMark, configuration, get_queue_messages, make_testable_runner,
specialized_message_from_string as mfs)
from mailman.testing.layers import ConfigLayer
@@ -257,7 +251,7 @@ Testing
@mock.patch('nntplib.NNTP')
def test_connect(self, class_mock):
# Test connection to the NNTP server with default values.
- self._nntpq.enqueue(self._msg, {}, listname='test@example.com')
+ self._nntpq.enqueue(self._msg, {}, listid='test.example.com')
self._runner.run()
class_mock.assert_called_once_with(
'', 119, user='', password='', readermode=True)
@@ -267,7 +261,7 @@ Testing
@mock.patch('nntplib.NNTP')
def test_connect_with_configuration(self, class_mock):
# Test connection to the NNTP server with specific values.
- self._nntpq.enqueue(self._msg, {}, listname='test@example.com')
+ self._nntpq.enqueue(self._msg, {}, listid='test.example.com')
self._runner.run()
class_mock.assert_called_once_with(
'nntp.example.com', 2112,
@@ -276,7 +270,7 @@ Testing
@mock.patch('nntplib.NNTP')
def test_post(self, class_mock):
# Test that the message is posted to the NNTP server.
- self._nntpq.enqueue(self._msg, {}, listname='test@example.com')
+ self._nntpq.enqueue(self._msg, {}, listid='test.example.com')
self._runner.run()
# Get the mocked instance, which was used in the runner.
conn_mock = class_mock()
@@ -295,7 +289,7 @@ Testing
def test_connection_got_quit(self, class_mock):
# The NNTP connection gets closed after a successful post.
# Test that the message is posted to the NNTP server.
- self._nntpq.enqueue(self._msg, {}, listname='test@example.com')
+ self._nntpq.enqueue(self._msg, {}, listid='test.example.com')
self._runner.run()
# Get the mocked instance, which was used in the runner.
conn_mock = class_mock()
@@ -304,18 +298,19 @@ Testing
# and make some simple checks that the message is what we expected.
conn_mock.quit.assert_called_once_with()
- @mock.patch('nntplib.NNTP', side_effect=nntplib.error_temp)
+ @mock.patch('nntplib.NNTP', side_effect=nntplib.NNTPTemporaryError)
def test_connect_with_nntplib_failure(self, class_mock):
- self._nntpq.enqueue(self._msg, {}, listname='test@example.com')
+ self._nntpq.enqueue(self._msg, {}, listid='test.example.com')
mark = LogFileMark('mailman.error')
self._runner.run()
log_message = mark.readline()[:-1]
- self.assertTrue(log_message.endswith(
- 'NNTP error for test@example.com'))
+ self.assertTrue(
+ log_message.endswith('NNTP error for test@example.com'),
+ log_message)
@mock.patch('nntplib.NNTP', side_effect=socket.error)
def test_connect_with_socket_failure(self, class_mock):
- self._nntpq.enqueue(self._msg, {}, listname='test@example.com')
+ self._nntpq.enqueue(self._msg, {}, listid='test.example.com')
mark = LogFileMark('mailman.error')
self._runner.run()
log_message = mark.readline()[:-1]
@@ -330,7 +325,7 @@ Testing
# I.e. stop immediately, since the queue will not be empty.
return True
runner = make_testable_runner(nntp.NNTPRunner, 'nntp', predicate=once)
- self._nntpq.enqueue(self._msg, {}, listname='test@example.com')
+ self._nntpq.enqueue(self._msg, {}, listid='test.example.com')
mark = LogFileMark('mailman.error')
runner.run()
log_message = mark.readline()[:-1]
@@ -338,14 +333,14 @@ Testing
'NNTP unexpected exception for test@example.com'))
messages = get_queue_messages('nntp')
self.assertEqual(len(messages), 1)
- self.assertEqual(messages[0].msgdata['listname'], 'test@example.com')
+ self.assertEqual(messages[0].msgdata['listid'], 'test.example.com')
self.assertEqual(messages[0].msg['subject'], 'A newsgroup posting')
- @mock.patch('nntplib.NNTP', side_effect=nntplib.error_temp)
+ @mock.patch('nntplib.NNTP', side_effect=nntplib.NNTPTemporaryError)
def test_connection_never_gets_quit_after_failures(self, class_mock):
# The NNTP connection doesn't get closed after a unsuccessful
# connection, since there's nothing to close.
- self._nntpq.enqueue(self._msg, {}, listname='test@example.com')
+ self._nntpq.enqueue(self._msg, {}, listid='test.example.com')
self._runner.run()
# Get the mocked instance, which was used in the runner. Turn off the
# exception raising side effect first though!
@@ -361,8 +356,8 @@ Testing
# The NNTP connection does get closed after a unsuccessful post.
# Add a side-effect to the instance mock's .post() method.
conn_mock = class_mock()
- conn_mock.post.side_effect = nntplib.error_temp
- self._nntpq.enqueue(self._msg, {}, listname='test@example.com')
+ conn_mock.post.side_effect = nntplib.NNTPTemporaryError
+ self._nntpq.enqueue(self._msg, {}, listid='test.example.com')
self._runner.run()
# The connection object's post() method was called once with a
# file-like object containing the message's bytes. Read those bytes
diff --git a/src/mailman/runners/tests/test_outgoing.py b/src/mailman/runners/tests/test_outgoing.py
index 62f6776b1..8f51c4ce2 100644
--- a/src/mailman/runners/tests/test_outgoing.py
+++ b/src/mailman/runners/tests/test_outgoing.py
@@ -17,10 +17,11 @@
"""Test the outgoing runner."""
-from __future__ import absolute_import, print_function, unicode_literals
-
-__metaclass__ = type
__all__ = [
+ 'TestOnce',
+ 'TestSocketError',
+ 'TestSomeRecipientsFailed',
+ 'TestVERPSettings',
]
@@ -32,8 +33,6 @@ import unittest
from contextlib import contextmanager
from datetime import datetime, timedelta
from lazr.config import as_timedelta
-from zope.component import getUtility
-
from mailman.app.bounces import send_probe
from mailman.app.lifecycle import create_list
from mailman.config import config
@@ -45,12 +44,11 @@ from mailman.interfaces.pending import IPendings
from mailman.interfaces.usermanager import IUserManager
from mailman.runners.outgoing import OutgoingRunner
from mailman.testing.helpers import (
- LogFileMark,
- get_queue_messages,
- make_testable_runner,
+ LogFileMark, get_queue_messages, make_testable_runner,
specialized_message_from_string as message_from_string)
from mailman.testing.layers import ConfigLayer, SMTPLayer
from mailman.utilities.datetime import factory, now
+from zope.component import getUtility
@@ -96,7 +94,7 @@ Message-Id: <first>
deliver_after = now() + timedelta(days=10)
self._msgdata['deliver_after'] = deliver_after
self._outq.enqueue(self._msg, self._msgdata,
- tolist=True, listname='test@example.com')
+ tolist=True, listid='test.example.com')
self._runner.run()
items = get_queue_messages('out')
self.assertEqual(len(items), 1)
@@ -149,20 +147,20 @@ Message-Id: <first>
def test_delivery_callback(self):
# Test that the configuration variable calls the appropriate callback.
- self._outq.enqueue(self._msg, {}, listname='test@example.com')
+ self._outq.enqueue(self._msg, {}, listid='test.example.com')
self._runner.run()
self.assertEqual(captured_mlist, self._mlist)
self.assertEqual(captured_msg.as_string(), self._msg.as_string())
# Of course, the message metadata will contain a bunch of keys added
# by the processing. We don't really care about the details, so this
# test is a good enough stand-in.
- self.assertEqual(captured_msgdata['listname'], 'test@example.com')
+ self.assertEqual(captured_msgdata['listid'], 'test.example.com')
def test_verp_in_metadata(self):
# Test that if the metadata has a 'verp' key, it is unchanged.
marker = 'yepper'
msgdata = dict(verp=marker)
- self._outq.enqueue(self._msg, msgdata, listname='test@example.com')
+ self._outq.enqueue(self._msg, msgdata, listid='test.example.com')
self._runner.run()
self.assertEqual(captured_msgdata['verp'], marker)
@@ -171,7 +169,7 @@ Message-Id: <first>
# indicates, messages will be VERP'd.
msgdata = {}
self._mlist.personalize = Personalization.individual
- self._outq.enqueue(self._msg, msgdata, listname='test@example.com')
+ self._outq.enqueue(self._msg, msgdata, listid='test.example.com')
with temporary_config('personalize', """
[mta]
verp_personalized_deliveries: yes
@@ -184,7 +182,7 @@ Message-Id: <first>
# indicates, messages will be VERP'd.
msgdata = {}
self._mlist.personalize = Personalization.full
- self._outq.enqueue(self._msg, msgdata, listname='test@example.com')
+ self._outq.enqueue(self._msg, msgdata, listid='test.example.com')
with temporary_config('personalize', """
[mta]
verp_personalized_deliveries: yes
@@ -197,14 +195,14 @@ Message-Id: <first>
# does not indicate, messages will not be VERP'd.
msgdata = {}
self._mlist.personalize = Personalization.full
- self._outq.enqueue(self._msg, msgdata, listname='test@example.com')
+ self._outq.enqueue(self._msg, msgdata, listid='test.example.com')
self._runner.run()
self.assertFalse('verp' in captured_msgdata)
def test_verp_never(self):
# Never VERP when the interval is zero.
msgdata = {}
- self._outq.enqueue(self._msg, msgdata, listname='test@example.com')
+ self._outq.enqueue(self._msg, msgdata, listid='test.example.com')
with temporary_config('personalize', """
[mta]
verp_delivery_interval: 0
@@ -215,7 +213,7 @@ Message-Id: <first>
def test_verp_always(self):
# Always VERP when the interval is one.
msgdata = {}
- self._outq.enqueue(self._msg, msgdata, listname='test@example.com')
+ self._outq.enqueue(self._msg, msgdata, listid='test.example.com')
with temporary_config('personalize', """
[mta]
verp_delivery_interval: 1
@@ -227,7 +225,7 @@ Message-Id: <first>
# VERP every so often, when the post_id matches.
self._mlist.post_id = 5
msgdata = {}
- self._outq.enqueue(self._msg, msgdata, listname='test@example.com')
+ self._outq.enqueue(self._msg, msgdata, listid='test.example.com')
with temporary_config('personalize', """
[mta]
verp_delivery_interval: 5
@@ -239,7 +237,7 @@ Message-Id: <first>
# VERP every so often, when the post_id matches.
self._mlist.post_id = 4
msgdata = {}
- self._outq.enqueue(self._msg, msgdata, listname='test@example.com')
+ self._outq.enqueue(self._msg, msgdata, listid='test.example.com')
with temporary_config('personalize', """
[mta]
verp_delivery_interval: 5
@@ -287,7 +285,7 @@ Message-Id: <first>
error_log = logging.getLogger('mailman.error')
filename = error_log.handlers[0].filename
filepos = os.stat(filename).st_size
- self._outq.enqueue(self._msg, {}, listname='test@example.com')
+ self._outq.enqueue(self._msg, {}, listid='test.example.com')
with temporary_config('port 0', """
[mta]
smtp_port: 0
@@ -308,7 +306,7 @@ Message-Id: <first>
# that is a log message. Start by opening the error log and reading
# the current file position.
mark = LogFileMark('mailman.error')
- self._outq.enqueue(self._msg, {}, listname='test@example.com')
+ self._outq.enqueue(self._msg, {}, listid='test.example.com')
with temporary_config('port 0', """
[mta]
smtp_port: 2112
@@ -369,7 +367,7 @@ Message-Id: <first>
token = send_probe(member, self._msg)
msgdata = dict(probe_token=token)
permanent_failures.append('anne@example.com')
- self._outq.enqueue(self._msg, msgdata, listname='test@example.com')
+ self._outq.enqueue(self._msg, msgdata, listid='test.example.com')
self._runner.run()
events = list(self._processor.unprocessed)
self.assertEqual(len(events), 1)
@@ -390,7 +388,7 @@ Message-Id: <first>
getUtility(IPendings).confirm(token)
msgdata = dict(probe_token=token)
permanent_failures.append('anne@example.com')
- self._outq.enqueue(self._msg, msgdata, listname='test@example.com')
+ self._outq.enqueue(self._msg, msgdata, listid='test.example.com')
self._runner.run()
events = list(self._processor.unprocessed)
self.assertEqual(len(events), 0)
@@ -404,7 +402,7 @@ Message-Id: <first>
getUtility(IPendings).confirm(token)
msgdata = dict(probe_token=token)
temporary_failures.append('anne@example.com')
- self._outq.enqueue(self._msg, msgdata, listname='test@example.com')
+ self._outq.enqueue(self._msg, msgdata, listid='test.example.com')
self._runner.run()
events = list(self._processor.unprocessed)
self.assertEqual(len(events), 0)
@@ -412,7 +410,7 @@ Message-Id: <first>
def test_one_permanent_failure(self):
# Normal (i.e. non-probe) permanent failures just get registered.
permanent_failures.append('anne@example.com')
- self._outq.enqueue(self._msg, {}, listname='test@example.com')
+ self._outq.enqueue(self._msg, {}, listid='test.example.com')
self._runner.run()
events = list(self._processor.unprocessed)
self.assertEqual(len(events), 1)
@@ -423,7 +421,7 @@ Message-Id: <first>
# Two normal (i.e. non-probe) permanent failures just get registered.
permanent_failures.append('anne@example.com')
permanent_failures.append('bart@example.com')
- self._outq.enqueue(self._msg, {}, listname='test@example.com')
+ self._outq.enqueue(self._msg, {}, listid='test.example.com')
self._runner.run()
events = list(self._processor.unprocessed)
self.assertEqual(len(events), 2)
@@ -437,7 +435,7 @@ Message-Id: <first>
# put in the retry queue, but with some metadata to prevent infinite
# retries.
temporary_failures.append('cris@example.com')
- self._outq.enqueue(self._msg, {}, listname='test@example.com')
+ self._outq.enqueue(self._msg, {}, listid='test.example.com')
self._runner.run()
events = list(self._processor.unprocessed)
self.assertEqual(len(events), 0)
@@ -458,7 +456,7 @@ Message-Id: <first>
# retries.
temporary_failures.append('cris@example.com')
temporary_failures.append('dave@example.com')
- self._outq.enqueue(self._msg, {}, listname='test@example.com')
+ self._outq.enqueue(self._msg, {}, listid='test.example.com')
self._runner.run()
events = list(self._processor.unprocessed)
self.assertEqual(len(events), 0)
@@ -476,7 +474,7 @@ Message-Id: <first>
permanent_failures.append('fred@example.com')
temporary_failures.append('gwen@example.com')
temporary_failures.append('herb@example.com')
- self._outq.enqueue(self._msg, {}, listname='test@example.com')
+ self._outq.enqueue(self._msg, {}, listid='test.example.com')
self._runner.run()
# Let's look at the permanent failures.
events = list(self._processor.unprocessed)
@@ -503,7 +501,7 @@ Message-Id: <first>
as_timedelta(config.mta.delivery_retry_period))
msgdata = dict(last_recip_count=2,
deliver_until=deliver_until)
- self._outq.enqueue(self._msg, msgdata, listname='test@example.com')
+ self._outq.enqueue(self._msg, msgdata, listid='test.example.com')
self._runner.run()
# The retry queue should have our message waiting to be retried.
items = get_queue_messages('retry')
@@ -522,7 +520,7 @@ Message-Id: <first>
deliver_until = datetime(2005, 8, 1, 7, 49, 23) + retry_period
msgdata = dict(last_recip_count=2,
deliver_until=deliver_until)
- self._outq.enqueue(self._msg, msgdata, listname='test@example.com')
+ self._outq.enqueue(self._msg, msgdata, listid='test.example.com')
# Before the runner runs, several days pass.
factory.fast_forward(retry_period.days + 1)
mark = LogFileMark('mailman.smtp')
diff --git a/src/mailman/runners/tests/test_owner.py b/src/mailman/runners/tests/test_owner.py
index 6c68e91cc..15ca07c2e 100644
--- a/src/mailman/runners/tests/test_owner.py
+++ b/src/mailman/runners/tests/test_owner.py
@@ -22,9 +22,6 @@
# tests. They're not exactly integration tests, but they do touch lots of
# parts of the system.
-from __future__ import absolute_import, print_function, unicode_literals
-
-__metaclass__ = type
__all__ = [
'TestEmailToOwner',
]
@@ -32,22 +29,19 @@ __all__ = [
import unittest
-from operator import itemgetter
-from zope.component import getUtility
-
from mailman.app.lifecycle import create_list
from mailman.config import config
from mailman.database.transaction import transaction
from mailman.interfaces.member import MemberRole
from mailman.interfaces.usermanager import IUserManager
from mailman.testing.helpers import (
- TestableMaster,
- get_lmtp_client,
- make_testable_runner)
+ TestableMaster, get_lmtp_client, make_testable_runner)
from mailman.runners.incoming import IncomingRunner
from mailman.runners.outgoing import OutgoingRunner
from mailman.runners.pipeline import PipelineRunner
from mailman.testing.layers import SMTPLayer
+from operator import itemgetter
+from zope.component import getUtility
@@ -89,7 +83,7 @@ class TestEmailToOwner(unittest.TestCase):
# get a copy of the message.
lmtp = get_lmtp_client(quiet=True)
lmtp.lhlo('remote.example.org')
- lmtp.sendmail('zuzu@example.org', ['test-owner@example.com'], """\
+ lmtp.sendmail('zuzu@example.org', ['test-owner@example.com'], b"""\
From: Zuzu Person <zuzu@example.org>
To: test-owner@example.com
Message-ID: <ant>
diff --git a/src/mailman/runners/tests/test_pipeline.py b/src/mailman/runners/tests/test_pipeline.py
index 50ec6cb9a..347bde16b 100644
--- a/src/mailman/runners/tests/test_pipeline.py
+++ b/src/mailman/runners/tests/test_pipeline.py
@@ -17,9 +17,6 @@
"""Test the pipeline runner."""
-from __future__ import absolute_import, print_function, unicode_literals
-
-__metaclass__ = type
__all__ = [
'TestPipelineRunner',
]
@@ -27,17 +24,15 @@ __all__ = [
import unittest
-from zope.interface import implementer
-
from mailman.app.lifecycle import create_list
from mailman.config import config
from mailman.interfaces.handler import IHandler
from mailman.interfaces.pipeline import IPipeline
from mailman.runners.pipeline import PipelineRunner
from mailman.testing.helpers import (
- make_testable_runner,
- specialized_message_from_string as mfs)
+ make_testable_runner, specialized_message_from_string as mfs)
from mailman.testing.layers import ConfigLayer
+from zope.interface import implementer
@@ -101,7 +96,7 @@ To: test@example.com
def test_posting(self):
# A message accepted for posting gets processed through the posting
# pipeline.
- msgdata = dict(listname='test@example.com')
+ msgdata = dict(listid='test.example.com')
config.switchboards['pipeline'].enqueue(self._msg, msgdata)
self._pipeline.run()
self.assertEqual(len(self._markers), 1)
@@ -110,7 +105,7 @@ To: test@example.com
def test_owner(self):
# A message accepted for posting to a list's owners gets processed
# through the owner pipeline.
- msgdata = dict(listname='test@example.com',
+ msgdata = dict(listid='test.example.com',
to_owner=True)
config.switchboards['pipeline'].enqueue(self._msg, msgdata)
self._pipeline.run()
diff --git a/src/mailman/runners/tests/test_rest.py b/src/mailman/runners/tests/test_rest.py
index bbe026ad6..96ca19089 100644
--- a/src/mailman/runners/tests/test_rest.py
+++ b/src/mailman/runners/tests/test_rest.py
@@ -17,9 +17,6 @@
"""Test the REST runner."""
-from __future__ import absolute_import, print_function, unicode_literals
-
-__metaclass__ = type
__all__ = [
'TestRESTRunner',
]
diff --git a/src/mailman/runners/tests/test_retry.py b/src/mailman/runners/tests/test_retry.py
index 28289bc05..0a0929991 100644
--- a/src/mailman/runners/tests/test_retry.py
+++ b/src/mailman/runners/tests/test_retry.py
@@ -17,9 +17,6 @@
"""Test the retry runner."""
-from __future__ import absolute_import, print_function, unicode_literals
-
-__metaclass__ = type
__all__ = [
'TestRetryRunner',
]
@@ -31,8 +28,7 @@ from mailman.app.lifecycle import create_list
from mailman.config import config
from mailman.runners.retry import RetryRunner
from mailman.testing.helpers import (
- get_queue_messages,
- make_testable_runner,
+ get_queue_messages, make_testable_runner,
specialized_message_from_string as message_from_string)
from mailman.testing.layers import ConfigLayer
@@ -54,7 +50,7 @@ To: test@example.com
Message-Id: <first>
""")
- self._msgdata = dict(listname='test@example.com')
+ self._msgdata = dict(listid='test.example.com')
def test_message_put_in_outgoing_queue(self):
self._retryq.enqueue(self._msg, self._msgdata)
diff --git a/src/mailman/runners/virgin.py b/src/mailman/runners/virgin.py
index 0f91d61af..8ff45e86e 100644
--- a/src/mailman/runners/virgin.py
+++ b/src/mailman/runners/virgin.py
@@ -23,6 +23,11 @@ to go through some minimal processing before they can be sent out to the
recipient.
"""
+__all__ = [
+ 'VirginRunner',
+ ]
+
+
from mailman.core.pipelines import process
from mailman.core.runner import Runner
diff --git a/src/mailman/styles/base.py b/src/mailman/styles/base.py
index 0d65bbebb..db4072b5c 100644
--- a/src/mailman/styles/base.py
+++ b/src/mailman/styles/base.py
@@ -23,9 +23,6 @@ methods in your compositional derived class.
"""
-from __future__ import absolute_import, print_function, unicode_literals
-
-__metaclass__ = type
__all__ = [
'Announcement',
'BasicOperation',
@@ -38,7 +35,6 @@ __all__ = [
from datetime import timedelta
-
from mailman.core.i18n import _
from mailman.interfaces.action import Action, FilterAction
from mailman.interfaces.archiver import ArchivePolicy
diff --git a/src/mailman/styles/default.py b/src/mailman/styles/default.py
index b12999f0e..f7ea3447f 100644
--- a/src/mailman/styles/default.py
+++ b/src/mailman/styles/default.py
@@ -17,21 +17,17 @@
"""Application of list styles to new and existing lists."""
-from __future__ import absolute_import, print_function, unicode_literals
-
-__metaclass__ = type
__all__ = [
'LegacyDefaultStyle',
'LegacyAnnounceOnly',
]
-from zope.interface import implementer
-
from mailman.interfaces.styles import IStyle
from mailman.styles.base import (
Announcement, BasicOperation, Bounces, Discussion, Identity, Moderation,
Public)
+from zope.interface import implementer
diff --git a/src/mailman/styles/manager.py b/src/mailman/styles/manager.py
index 397902c17..59cbb1471 100644
--- a/src/mailman/styles/manager.py
+++ b/src/mailman/styles/manager.py
@@ -17,23 +17,19 @@
"""Style manager."""
-from __future__ import absolute_import, print_function, unicode_literals
-
-__metaclass__ = type
__all__ = [
'StyleManager',
'handle_ConfigurationUpdatedEvent',
]
-from zope.component import getUtility
-from zope.interface import implementer
-from zope.interface.verify import verifyObject
-
from mailman.interfaces.configuration import ConfigurationUpdatedEvent
from mailman.interfaces.styles import (
DuplicateStyleError, IStyle, IStyleManager)
from mailman.utilities.modules import find_components
+from zope.component import getUtility
+from zope.interface import implementer
+from zope.interface.verify import verifyObject
diff --git a/src/mailman/styles/tests/test_styles.py b/src/mailman/styles/tests/test_styles.py
index 1fb7a8410..8e8d2eb19 100644
--- a/src/mailman/styles/tests/test_styles.py
+++ b/src/mailman/styles/tests/test_styles.py
@@ -17,9 +17,6 @@
"""Test styles."""
-from __future__ import absolute_import, print_function, unicode_literals
-
-__metaclass__ = type
__all__ = [
'TestStyle',
]
@@ -27,13 +24,12 @@ __all__ = [
import unittest
-from zope.component import getUtility
-from zope.interface import implementer
-from zope.interface.exceptions import DoesNotImplement
-
from mailman.interfaces.styles import (
DuplicateStyleError, IStyle, IStyleManager)
from mailman.testing.layers import ConfigLayer
+from zope.component import getUtility
+from zope.interface import implementer
+from zope.interface.exceptions import DoesNotImplement
diff --git a/src/mailman/testing/documentation.py b/src/mailman/testing/documentation.py
index b8d852fed..e7511fb9b 100644
--- a/src/mailman/testing/documentation.py
+++ b/src/mailman/testing/documentation.py
@@ -21,9 +21,6 @@ Note that doctest extraction does not currently work for zip file
distributions. doctest discovery currently requires file system traversal.
"""
-from __future__ import absolute_import, print_function, unicode_literals
-
-__metaclass__ = type
__all__ = [
'setup',
'teardown'
@@ -31,7 +28,6 @@ __all__ = [
from inspect import isfunction, ismethod
-
from mailman.app.lifecycle import create_list
from mailman.config import config
from mailman.testing.helpers import call_api, specialized_message_from_string
@@ -145,11 +141,6 @@ def dump_json(url, data=None, method=None, username=None, password=None):
def setup(testobj):
"""Test setup."""
- # Make sure future statements in our doctests are the same as everywhere
- # else.
- testobj.globs['absolute_import'] = absolute_import
- testobj.globs['print_function'] = print_function
- testobj.globs['unicode_literals'] = unicode_literals
# In general, I don't like adding convenience functions, since I think
# doctests should do the imports themselves. It makes for better
# documentation that way. However, a few are really useful, or help to
diff --git a/src/mailman/testing/helpers.py b/src/mailman/testing/helpers.py
index 38e210b06..b00534490 100644
--- a/src/mailman/testing/helpers.py
+++ b/src/mailman/testing/helpers.py
@@ -17,9 +17,6 @@
"""Various test helpers."""
-from __future__ import absolute_import, print_function, unicode_literals
-
-__metaclass__ = type
__all__ = [
'LogFileMark',
'TestableMaster',
@@ -60,11 +57,6 @@ from contextlib import contextmanager
from email import message_from_string
from httplib2 import Http
from lazr.config import as_timedelta
-from urllib import urlencode
-from urllib2 import HTTPError
-from zope import event
-from zope.component import getUtility
-
from mailman.bin.master import Loop as Master
from mailman.config import config
from mailman.database.transaction import transaction
@@ -75,6 +67,10 @@ from mailman.interfaces.styles import IStyleManager
from mailman.interfaces.usermanager import IUserManager
from mailman.runners.digest import DigestRunner
from mailman.utilities.mailbox import Mailbox
+from six.moves.urllib_error import HTTPError
+from six.moves.urllib_parse import urlencode
+from zope import event
+from zope.component import getUtility
NL = '\n'
@@ -335,7 +331,10 @@ def call_api(url, data=None, method=None, username=None, password=None):
basic_auth = '{0}:{1}'.format(
(config.webservice.admin_user if username is None else username),
(config.webservice.admin_pass if password is None else password))
- headers['Authorization'] = 'Basic ' + b64encode(basic_auth)
+ # b64encode() requires a bytes, but the header value must be str. Do the
+ # necessary conversion dances.
+ token = b64encode(basic_auth.encode('utf-8')).decode('ascii')
+ headers['Authorization'] = 'Basic ' + token
response, content = Http().request(url, method, data, headers)
# If we did not get a 2xx status code, make this look like a urllib2
# exception, for backward compatibility with existing doctests.
@@ -470,10 +469,11 @@ def reset_the_world():
"""
# Reset the database between tests.
config.db._reset()
- # Remove any digest files.
+ # Remove any digest files and members.txt file (for the file-recips
+ # handler) in the lists' data directories.
for dirpath, dirnames, filenames in os.walk(config.LIST_DATA_DIR):
for filename in filenames:
- if filename.endswith('.mmdf'):
+ if filename.endswith('.mmdf') or filename == 'members.txt':
os.remove(os.path.join(dirpath, filename))
# Remove all residual queue files.
for dirpath, dirnames, filenames in os.walk(config.QUEUE_DIR):
@@ -508,9 +508,8 @@ def specialized_message_from_string(unicode_text):
"""
# This mimic what Switchboard.dequeue() does when parsing a message from
# text into a Message instance.
- text = unicode_text.encode('ascii')
- original_size = len(text)
- message = message_from_string(text, Message)
+ original_size = len(unicode_text)
+ message = message_from_string(unicode_text, Message)
message.original_size = original_size
return message
diff --git a/src/mailman/testing/i18n.py b/src/mailman/testing/i18n.py
index 933a5ec0f..6718f5dda 100644
--- a/src/mailman/testing/i18n.py
+++ b/src/mailman/testing/i18n.py
@@ -17,9 +17,6 @@
"""Internationalization for the tests."""
-from __future__ import absolute_import, print_function, unicode_literals
-
-__metaclass__ = type
__all__ = [
'TestingStrategy',
'initialize',
@@ -29,9 +26,8 @@ __all__ = [
from contextlib import closing
from flufl.i18n import registry
from gettext import GNUTranslations, NullTranslations
-from pkg_resources import resource_stream
-
from mailman.core.i18n import initialize as core_initialize
+from pkg_resources import resource_stream
diff --git a/src/mailman/testing/layers.py b/src/mailman/testing/layers.py
index 74ad99dc8..d38878160 100644
--- a/src/mailman/testing/layers.py
+++ b/src/mailman/testing/layers.py
@@ -20,14 +20,10 @@
# XXX 2012-03-23 BAW: Layers really really suck. For example, the
# test_owners_get_email() test requires that both the SMTPLayer and LMTPLayer
# be set up, but there's apparently no way to do that and make zope.testing
-# happy. This causes no tests failures, but it does cause errors at the end
-# of the full test run. For now, I'll ignore that, but I do want to
-# eventually get rid of the zope.test* dependencies and use something like
-# testresources or some such.
+# happy. This causes no test failures, but it does cause errors at the end of
+# the full test run. For now, I'll ignore that, but I do want to eventually
+# get rid of the layers and use something like testresources or some such.
-from __future__ import absolute_import, print_function, unicode_literals
-
-__metaclass__ = type
__all__ = [
'ConfigLayer',
'LMTPLayer',
@@ -46,10 +42,6 @@ import datetime
import tempfile
from lazr.config import as_boolean
-from pkg_resources import resource_string
-from textwrap import dedent
-from zope.component import getUtility
-
from mailman.config import config
from mailman.core import initialize
from mailman.core.initialize import INHIBIT_CONFIG_FILE
@@ -60,6 +52,9 @@ from mailman.testing.helpers import (
TestableMaster, get_lmtp_client, reset_the_world, wait_for_webservice)
from mailman.testing.mta import ConnectionCountingController
from mailman.utilities.string import expand
+from pkg_resources import resource_string as resource_bytes
+from textwrap import dedent
+from zope.component import getUtility
TEST_TIMEOUT = datetime.timedelta(seconds=5)
@@ -132,7 +127,8 @@ class ConfigLayer(MockAndMonkeyLayer):
configuration: {1}
""".format(cls.var_dir, postfix_cfg))
# Read the testing config and push it.
- test_config += resource_string('mailman.testing', 'testing.cfg')
+ more = resource_bytes('mailman.testing', 'testing.cfg')
+ test_config += more.decode('utf-8')
config.create_paths = True
config.push('test config', test_config)
# Initialize everything else.
diff --git a/src/mailman/testing/mta.py b/src/mailman/testing/mta.py
index 875647485..81a6bf1ac 100644
--- a/src/mailman/testing/mta.py
+++ b/src/mailman/testing/mta.py
@@ -17,9 +17,6 @@
"""Fake MTA for testing purposes."""
-from __future__ import absolute_import, print_function, unicode_literals
-
-__metaclass__ = type
__all__ = [
'FakeMTA',
]
@@ -27,13 +24,11 @@ __all__ = [
import logging
-from Queue import Empty, Queue
-
from lazr.smtptest.controller import QueueController
from lazr.smtptest.server import Channel, QueueServer
-from zope.interface import implementer
-
from mailman.interfaces.mta import IMailTransportAgentLifecycle
+from six.moves.queue import Empty, Queue
+from zope.interface import implementer
log = logging.getLogger('lazr.smtptest')
@@ -60,28 +55,28 @@ class StatisticsChannel(Channel):
def smtp_EHLO(self, arg):
if not arg:
- self.push(b'501 Syntax: HELO hostname')
+ self.push('501 Syntax: HELO hostname')
return
if self._SMTPChannel__greeting:
- self.push(b'503 Duplicate HELO/EHLO')
+ self.push('503 Duplicate HELO/EHLO')
else:
self._SMTPChannel__greeting = arg
- self.push(b'250-%s' % self._SMTPChannel__fqdn)
- self.push(b'250 AUTH PLAIN')
+ self.push('250-%s' % self._SMTPChannel__fqdn)
+ self.push('250 AUTH PLAIN')
def smtp_STAT(self, arg):
"""Cause the server to send statistics to its controller."""
self._server.send_statistics()
- self.push(b'250 Ok')
+ self.push('250 Ok')
def smtp_AUTH(self, arg):
"""Record that the AUTH occurred."""
if arg == 'PLAIN AHRlc3R1c2VyAHRlc3RwYXNz':
# testuser:testpass
- self.push(b'235 Ok')
+ self.push('235 Ok')
self._server.send_auth(arg)
else:
- self.push(b'571 Bad authentication')
+ self.push('571 Bad authentication')
def smtp_RCPT(self, arg):
"""For testing, sometimes cause a non-25x response."""
@@ -92,7 +87,7 @@ class StatisticsChannel(Channel):
else:
# The test suite wants this to fail. The message corresponds to
# the exception we expect smtplib.SMTP to raise.
- self.push(b'%d Error: SMTPRecipientsRefused' % code)
+ self.push('%d Error: SMTPRecipientsRefused' % code)
def smtp_MAIL(self, arg):
"""For testing, sometimes cause a non-25x response."""
@@ -103,7 +98,7 @@ class StatisticsChannel(Channel):
else:
# The test suite wants this to fail. The message corresponds to
# the exception we expect smtplib.SMTP to raise.
- self.push(b'%d Error: SMTPResponseException' % code)
+ self.push('%d Error: SMTPResponseException' % code)
@@ -211,7 +206,7 @@ class ConnectionCountingController(QueueController):
:rtype: integer
"""
smtpd = self._connect()
- smtpd.docmd(b'STAT')
+ smtpd.docmd('STAT')
# An Empty exception will occur if the data isn't available in 10
# seconds. Let that propagate.
return self.oob_queue.get(block=True, timeout=10)
@@ -232,4 +227,4 @@ class ConnectionCountingController(QueueController):
def reset(self):
smtpd = self._connect()
- smtpd.docmd(b'RSET')
+ smtpd.docmd('RSET')
diff --git a/src/mailman/testing/nose.py b/src/mailman/testing/nose.py
index 8fe7017c0..181048b64 100644
--- a/src/mailman/testing/nose.py
+++ b/src/mailman/testing/nose.py
@@ -17,9 +17,6 @@
"""nose2 test infrastructure."""
-from __future__ import absolute_import, print_function, unicode_literals
-
-__metaclass__ = type
__all__ = [
'NosePlugin',
]
@@ -35,6 +32,7 @@ from mailman.testing.documentation import setup, teardown
from mailman.testing.layers import ConfigLayer, MockAndMonkeyLayer, SMTPLayer
from nose2.events import Plugin
+
DOT = '.'
FLAGS = doctest.ELLIPSIS | doctest.NORMALIZE_WHITESPACE | doctest.REPORT_NDIFF
TOPDIR = os.path.dirname(mailman.__file__)
@@ -116,3 +114,9 @@ class NosePlugin(Plugin):
# Suppress the extra "Doctest: ..." line.
test.shortDescription = lambda: None
event.extraTests.append(test)
+
+ ## def startTest(self, event):
+ ## import sys; print('vvvvv', event.test, file=sys.stderr)
+
+ ## def stopTest(self, event):
+ ## import sys; print('^^^^^', event.test, file=sys.stderr)
diff --git a/src/mailman/tests/test_configfile.py b/src/mailman/tests/test_configfile.py
index 22442c767..0807c0648 100644
--- a/src/mailman/tests/test_configfile.py
+++ b/src/mailman/tests/test_configfile.py
@@ -17,10 +17,10 @@
"""Test configuration file searching."""
-from __future__ import absolute_import, print_function, unicode_literals
-
-__metaclass__ = type
__all__ = [
+ 'TestConfigFileBase',
+ 'TestConfigFileSearch',
+ 'TestConfigFileSearchWithChroot',
]
@@ -31,7 +31,6 @@ import tempfile
import unittest
from contextlib import contextmanager
-
from mailman.core.initialize import search_for_configuration_file
@@ -107,6 +106,7 @@ class TestConfigFileBase(unittest.TestCase):
return os.path.join(self._root, path)
+
class TestConfigFileSearch(TestConfigFileBase):
"""Test various aspects of searching for configuration files.
@@ -128,6 +128,7 @@ class TestConfigFileSearch(TestConfigFileBase):
self.assertEqual(found, config_file)
+
class TestConfigFileSearchWithChroot(TestConfigFileBase):
"""Like `TestConfigFileSearch` but with a special os.path.exists()."""
diff --git a/src/mailman/utilities/datetime.py b/src/mailman/utilities/datetime.py
index b494e2513..3cea0d0cd 100644
--- a/src/mailman/utilities/datetime.py
+++ b/src/mailman/utilities/datetime.py
@@ -22,10 +22,6 @@ datetime.datetime.now() and datetime.date.today(). These are better
instrumented for testing purposes.
"""
-
-from __future__ import absolute_import, print_function, unicode_literals
-
-__metaclass__ = type
__all__ = [
'DateFactory',
'RFC822_DATE_FMT',
diff --git a/src/mailman/utilities/email.py b/src/mailman/utilities/email.py
index ea44ad0a4..bedbd2ae9 100644
--- a/src/mailman/utilities/email.py
+++ b/src/mailman/utilities/email.py
@@ -17,9 +17,6 @@
"""Email helpers."""
-from __future__ import absolute_import, print_function, unicode_literals
-
-__metaclass__ = type
__all__ = [
'add_message_hash',
'split_email',
@@ -70,7 +67,10 @@ def add_message_hash(msg):
message_id = message_id[1:-1]
else:
message_id = message_id.strip()
- digest = sha1(message_id).digest()
+ # Because .digest() returns bytes, b32encode() will return bytes, however
+ # we need a string for the header value. We know the b32encoded byte
+ # string must be ascii-only.
+ digest = sha1(message_id.encode('utf-8')).digest()
message_id_hash = b32encode(digest)
del msg['x-message-id-hash']
- msg['X-Message-ID-Hash'] = message_id_hash
+ msg['X-Message-ID-Hash'] = message_id_hash.decode('ascii')
diff --git a/src/mailman/utilities/filesystem.py b/src/mailman/utilities/filesystem.py
index f2a5b705b..4ef52cbfa 100644
--- a/src/mailman/utilities/filesystem.py
+++ b/src/mailman/utilities/filesystem.py
@@ -17,9 +17,6 @@
"""Filesystem utilities."""
-from __future__ import absolute_import, print_function, unicode_literals
-
-__metaclass__ = type
__all__ = [
'makedirs',
'umask',
diff --git a/src/mailman/utilities/i18n.py b/src/mailman/utilities/i18n.py
index e22bd6c18..16f2fee6b 100644
--- a/src/mailman/utilities/i18n.py
+++ b/src/mailman/utilities/i18n.py
@@ -17,9 +17,6 @@
"""i18n template search and interpolation."""
-from __future__ import absolute_import, print_function, unicode_literals
-
-__metaclass__ = type
__all__ = [
'TemplateNotFoundError',
'find',
@@ -29,17 +26,17 @@ __all__ = [
import os
+import six
import sys
import errno
from itertools import product
-from pkg_resources import resource_filename
-
from mailman.config import config
from mailman.core.constants import system_preferences
from mailman.core.errors import MailmanException
from mailman.core.i18n import _
from mailman.utilities.string import expand, wrap as wrap_text
+from pkg_resources import resource_filename
@@ -203,7 +200,8 @@ def make(template_file, mlist=None, language=None, wrap=True,
template = _(fp.read()[:-1])
finally:
fp.close()
- assert isinstance(template, unicode), 'Translated template is not unicode'
+ assert isinstance(template, six.text_type), (
+ 'Translated template is not unicode')
text = expand(template, kw)
if wrap:
return wrap_text(text)
diff --git a/src/mailman/utilities/importer.py b/src/mailman/utilities/importer.py
index cc8a0cf44..2db5f3ace 100644
--- a/src/mailman/utilities/importer.py
+++ b/src/mailman/utilities/importer.py
@@ -17,9 +17,6 @@
"""Importer routines."""
-from __future__ import absolute_import, print_function, unicode_literals
-
-__metaclass__ = type
__all__ = [
'Import21Error',
'import_config_pck',
@@ -48,7 +45,7 @@ from mailman.interfaces.nntp import NewsgroupModeration
from mailman.interfaces.usermanager import IUserManager
from mailman.utilities.filesystem import makedirs
from mailman.utilities.i18n import search
-from urllib2 import URLError
+from six.moves.urllib_error import URLError
from zope.component import getUtility
@@ -58,7 +55,7 @@ class Import21Error(MailmanError):
-def str_to_unicode(value):
+def bytes_to_str(value):
# Convert a string to unicode when the encoding is not declared.
if not isinstance(value, bytes):
return value
@@ -71,8 +68,10 @@ def str_to_unicode(value):
return value.decode('ascii', 'replace')
-def unicode_to_string(value):
- return None if value is None else str(value)
+def str_to_bytes(value):
+ if value is None or isinstance(value, bytes):
+ return value
+ return value.encode('utf-8')
def seconds_to_delta(value):
@@ -84,7 +83,7 @@ def days_to_delta(value):
def list_members_to_unicode(value):
- return [str_to_unicode(item) for item in value]
+ return [bytes_to_str(item) for item in value]
@@ -132,7 +131,7 @@ def nonmember_action_mapping(value):
def check_language_code(code):
if code is None:
return None
- code = str_to_unicode(code)
+ code = bytes_to_str(code)
if code not in getUtility(ILanguageManager):
msg = """Missing language: {0}
You must add a section describing this language to your mailman.cfg file.
@@ -170,7 +169,7 @@ TYPES = dict(
forward_unrecognized_bounces_to=UnrecognizedBounceDisposition,
gateway_to_mail=bool,
include_rfc2369_headers=bool,
- moderator_password=unicode_to_string,
+ moderator_password=str_to_bytes,
newsgroup_moderation=NewsgroupModeration,
nntp_prefix_subject_too=bool,
pass_extensions=list_members_to_unicode,
@@ -213,8 +212,10 @@ DATETIME_COLUMNS = [
]
EXCLUDES = set((
+ 'delivery_status',
'digest_members',
'members',
+ 'user_options',
))
@@ -243,9 +244,9 @@ def import_config_pck(mlist, config_dict):
# If the mailing list has a preferred language that isn't registered
# in the configuration file, hasattr() will swallow the KeyError this
# raises and return False. Treat that attribute specially.
- if hasattr(mlist, key) or key == 'preferred_language':
- if isinstance(value, str):
- value = str_to_unicode(value)
+ if key == 'preferred_language' or hasattr(mlist, key):
+ if isinstance(value, bytes):
+ value = bytes_to_str(value)
# Some types require conversion.
converter = TYPES.get(key)
try:
@@ -279,17 +280,19 @@ def import_config_pck(mlist, config_dict):
# Handle ban list.
ban_manager = IBanManager(mlist)
for address in config_dict.get('ban_list', []):
- ban_manager.ban(str_to_unicode(address))
+ ban_manager.ban(bytes_to_str(address))
# Handle acceptable aliases.
acceptable_aliases = config_dict.get('acceptable_aliases', '')
- if isinstance(acceptable_aliases, basestring):
+ if isinstance(acceptable_aliases, bytes):
+ acceptable_aliases = acceptable_aliases.decode('utf-8')
+ if isinstance(acceptable_aliases, str):
acceptable_aliases = acceptable_aliases.splitlines()
alias_set = IAcceptableAliasSet(mlist)
for address in acceptable_aliases:
address = address.strip()
if len(address) == 0:
continue
- address = str_to_unicode(address)
+ address = bytes_to_str(address)
try:
alias_set.add(address)
except ValueError:
@@ -343,7 +346,8 @@ def import_config_pck(mlist, config_dict):
if oldvar not in config_dict:
continue
text = config_dict[oldvar]
- text = text.decode('utf-8', 'replace')
+ if isinstance(text, bytes):
+ text = text.decode('utf-8', 'replace')
for oldph, newph in convert_placeholders:
text = text.replace(oldph, newph)
default_value, default_text = defaults.get(newvar, (None, None))
@@ -380,8 +384,9 @@ def import_config_pck(mlist, config_dict):
with codecs.open(filepath, 'w', encoding='utf-8') as fp:
fp.write(text)
# Import rosters.
- members = set(config_dict.get('members', {}).keys()
- + config_dict.get('digest_members', {}).keys())
+ regulars_set = set(config_dict.get('members', {}))
+ digesters_set = set(config_dict.get('digest_members', {}))
+ members = regulars_set.union(digesters_set)
import_roster(mlist, config_dict, members, MemberRole.member)
import_roster(mlist, config_dict, config_dict.get('owner', []),
MemberRole.owner)
@@ -407,7 +412,7 @@ def import_roster(mlist, config_dict, members, role):
for email in members:
# For owners and members, the emails can have a mixed case, so
# lowercase them all.
- email = str_to_unicode(email).lower()
+ email = bytes_to_str(email).lower()
if roster.get_member(email) is not None:
print('{} is already imported with role {}'.format(email, role),
file=sys.stderr)
@@ -421,7 +426,7 @@ def import_roster(mlist, config_dict, members, role):
merged_members.update(config_dict.get('members', {}))
merged_members.update(config_dict.get('digest_members', {}))
if merged_members.get(email, 0) != 0:
- original_email = str_to_unicode(merged_members[email])
+ original_email = bytes_to_str(merged_members[email])
else:
original_email = email
address = usermanager.create_address(original_email)
@@ -449,9 +454,9 @@ def import_roster(mlist, config_dict, members, role):
# overwritten.
if email in config_dict.get('usernames', {}):
address.display_name = \
- str_to_unicode(config_dict['usernames'][email])
+ bytes_to_str(config_dict['usernames'][email])
user.display_name = \
- str_to_unicode(config_dict['usernames'][email])
+ bytes_to_str(config_dict['usernames'][email])
if email in config_dict.get('passwords', {}):
user.password = config.password_context.encrypt(
config_dict['passwords'][email])
diff --git a/src/mailman/utilities/interact.py b/src/mailman/utilities/interact.py
index 8bca9ee40..c7531f302 100644
--- a/src/mailman/utilities/interact.py
+++ b/src/mailman/utilities/interact.py
@@ -17,9 +17,6 @@
"""Provide an interactive prompt, mimicking the Python interpreter."""
-from __future__ import absolute_import, print_function, unicode_literals
-
-__metaclass__ = type
__all__ = [
'interact',
]
@@ -78,9 +75,3 @@ Type "help", "copyright", "credits" or "license" for more information.''' % (
elif not banner:
banner = None
interp.interact(banner)
- # When an exception occurs in the InteractiveConsole, the various
- # sys.exc_* attributes get set so that error handling works the same way
- # there as it does in the built-in interpreter. Be anal about clearing
- # any exception information before we're done.
- sys.exc_clear()
- sys.last_type = sys.last_value = sys.last_traceback = None
diff --git a/src/mailman/utilities/mailbox.py b/src/mailman/utilities/mailbox.py
index 4f085e127..71e083792 100644
--- a/src/mailman/utilities/mailbox.py
+++ b/src/mailman/utilities/mailbox.py
@@ -15,11 +15,8 @@
# You should have received a copy of the GNU General Public License along with
# GNU Mailman. If not, see <http://www.gnu.org/licenses/>.
-"""Module stuff."""
+"""MMDF helper for digests."""
-from __future__ import absolute_import, print_function, unicode_literals
-
-__metaclass__ = type
__all__ = [
'Mailbox',
]
diff --git a/src/mailman/utilities/modules.py b/src/mailman/utilities/modules.py
index 9ff0e50cd..2a63ac501 100644
--- a/src/mailman/utilities/modules.py
+++ b/src/mailman/utilities/modules.py
@@ -17,9 +17,6 @@
"""Package and module utilities."""
-from __future__ import absolute_import, print_function, unicode_literals
-
-__metaclass__ = type
__all__ = [
'call_name',
'expand_path',
diff --git a/src/mailman/utilities/passwords.py b/src/mailman/utilities/passwords.py
index 6fb7f08c0..f29482572 100644
--- a/src/mailman/utilities/passwords.py
+++ b/src/mailman/utilities/passwords.py
@@ -17,19 +17,14 @@
"""A wrapper around passlib."""
-from __future__ import absolute_import, print_function, unicode_literals
-
-__metaclass__ = type
__all__ = [
'handle_ConfigurationUpdatedEvent',
]
-
-from passlib.context import CryptContext
-
from mailman.config.config import load_external
from mailman.interfaces.configuration import ConfigurationUpdatedEvent
+from passlib.context import CryptContext
diff --git a/src/mailman/utilities/string.py b/src/mailman/utilities/string.py
index d6f0da286..6bbf3c6ea 100644
--- a/src/mailman/utilities/string.py
+++ b/src/mailman/utilities/string.py
@@ -17,9 +17,6 @@
"""String utilities."""
-from __future__ import absolute_import, print_function, unicode_literals
-
-__metaclass__ = type
__all__ = [
'expand',
'oneline',
@@ -73,9 +70,8 @@ def oneline(s, cset='us-ascii', in_unicode=False):
:rtype: string
"""
try:
- h = make_header(decode_header(s))
- ustr = h.__unicode__()
- line = EMPTYSTRING.join(ustr.splitlines())
+ h = str(make_header(decode_header(s)))
+ line = EMPTYSTRING.join(h.splitlines())
if in_unicode:
return line
else:
diff --git a/src/mailman/utilities/tests/test_email.py b/src/mailman/utilities/tests/test_email.py
index 1448fb32b..838d50862 100644
--- a/src/mailman/utilities/tests/test_email.py
+++ b/src/mailman/utilities/tests/test_email.py
@@ -17,9 +17,6 @@
"""Testing functions in the email utilities."""
-from __future__ import absolute_import, print_function, unicode_literals
-
-__metaclass__ = type
__all__ = [
'TestEmail',
]
diff --git a/src/mailman/utilities/tests/test_import.py b/src/mailman/utilities/tests/test_import.py
index 42608ae45..192e08df5 100644
--- a/src/mailman/utilities/tests/test_import.py
+++ b/src/mailman/utilities/tests/test_import.py
@@ -17,26 +17,24 @@
"""Tests for config.pck imports."""
-from __future__ import absolute_import, print_function, unicode_literals
-
-__metaclass__ = type
__all__ = [
'TestArchiveImport',
'TestBasicImport',
+ 'TestConvertToURI',
+ 'TestFilterActionImport',
+ 'TestMemberActionImport',
+ 'TestPreferencesImport',
+ 'TestRosterImport',
]
import os
+import six
import mock
-import cPickle
import unittest
from datetime import timedelta, datetime
from enum import Enum
-from pkg_resources import resource_filename
-from sqlalchemy.exc import IntegrityError
-from zope.component import getUtility
-
from mailman.app.lifecycle import create_list
from mailman.config import config
from mailman.handlers.decorate import decorate
@@ -55,6 +53,9 @@ from mailman.testing.layers import ConfigLayer
from mailman.utilities.filesystem import makedirs
from mailman.utilities.importer import import_config_pck, Import21Error
from mailman.utilities.string import expand
+from pkg_resources import resource_filename
+from six.moves.cPickle import load
+from zope.component import getUtility
@@ -77,8 +78,8 @@ class TestBasicImport(unittest.TestCase):
def setUp(self):
self._mlist = create_list('blank@example.com')
pickle_file = resource_filename('mailman.testing', 'config.pck')
- with open(pickle_file) as fp:
- self._pckdict = cPickle.load(fp)
+ with open(pickle_file, 'rb') as fp:
+ self._pckdict = load(fp)
def _import(self):
import_config_pck(self._mlist, self._pckdict)
@@ -180,15 +181,15 @@ class TestBasicImport(unittest.TestCase):
def test_moderator_password(self):
# mod_password -> moderator_password
- self._mlist.moderator_password = str('TESTDATA')
+ self._mlist.moderator_password = b'TESTDATA'
self._import()
self.assertEqual(self._mlist.moderator_password, None)
def test_moderator_password_str(self):
# moderator_password must not be unicode
- self._pckdict[b'mod_password'] = b'TESTVALUE'
+ self._pckdict['mod_password'] = b'TESTVALUE'
self._import()
- self.assertFalse(isinstance(self._mlist.moderator_password, unicode))
+ self.assertNotIsInstance(self._mlist.moderator_password, six.text_type)
self.assertEqual(self._mlist.moderator_password, b'TESTVALUE')
def test_newsgroup_moderation(self):
@@ -227,7 +228,7 @@ class TestBasicImport(unittest.TestCase):
'alias2@exemple.com',
'non-ascii-\xe8@example.com',
]
- self._pckdict[b'acceptable_aliases'] = list_to_string(aliases)
+ self._pckdict['acceptable_aliases'] = list_to_string(aliases)
self._import()
alias_set = IAcceptableAliasSet(self._mlist)
self.assertEqual(sorted(alias_set.aliases), aliases)
@@ -236,7 +237,7 @@ class TestBasicImport(unittest.TestCase):
# Values without an '@' sign used to be matched against the local
# part, now we need to add the '^' sign to indicate it's a regexp.
aliases = ['invalid-value']
- self._pckdict[b'acceptable_aliases'] = list_to_string(aliases)
+ self._pckdict['acceptable_aliases'] = list_to_string(aliases)
self._import()
alias_set = IAcceptableAliasSet(self._mlist)
self.assertEqual(sorted(alias_set.aliases),
@@ -246,29 +247,31 @@ class TestBasicImport(unittest.TestCase):
# In some versions of the pickle, this can be a list, not a string
# (seen in the wild).
aliases = [b'alias1@example.com', b'alias2@exemple.com' ]
- self._pckdict[b'acceptable_aliases'] = aliases
+ self._pckdict['acceptable_aliases'] = aliases
self._import()
alias_set = IAcceptableAliasSet(self._mlist)
- self.assertEqual(sorted(alias_set.aliases), aliases)
+ self.assertEqual(sorted(alias_set.aliases),
+ sorted(a.decode('utf-8') for a in aliases))
def test_info_non_ascii(self):
# info can contain non-ascii characters.
info = 'O idioma aceito \xe9 somente Portugu\xeas do Brasil'
- self._pckdict[b'info'] = info.encode('utf-8')
+ self._pckdict['info'] = info.encode('utf-8')
self._import()
self.assertEqual(self._mlist.info, info,
'Encoding to UTF-8 is not handled')
# Test fallback to ascii with replace.
- self._pckdict[b'info'] = info.encode('iso-8859-1')
+ self._pckdict['info'] = info.encode('iso-8859-1')
# Suppress warning messages in test output.
with mock.patch('sys.stderr'):
self._import()
- self.assertEqual(self._mlist.info,
- unicode(self._pckdict[b'info'], 'ascii', 'replace'),
- "We don't fall back to replacing non-ascii chars")
+ self.assertEqual(
+ self._mlist.info,
+ self._pckdict['info'].decode('ascii', 'replace'),
+ "We don't fall back to replacing non-ascii chars")
def test_preferred_language(self):
- self._pckdict[b'preferred_language'] = b'ja'
+ self._pckdict['preferred_language'] = b'ja'
english = getUtility(ILanguageManager).get('en')
japanese = getUtility(ILanguageManager).get('ja')
self.assertEqual(self._mlist.preferred_language, english)
@@ -283,7 +286,7 @@ class TestBasicImport(unittest.TestCase):
self.assertEqual(self._mlist.preferred_language, english)
def test_new_language(self):
- self._pckdict[b'preferred_language'] = b'xx_XX'
+ self._pckdict['preferred_language'] = b'xx_XX'
try:
self._import()
except Import21Error as error:
@@ -409,35 +412,35 @@ class TestMemberActionImport(unittest.TestCase):
# Suppress warning messages in the test output.
with mock.patch('sys.stderr'):
import_config_pck(self._mlist, self._pckdict)
- for key, value in expected.iteritems():
+ for key, value in expected.items():
self.assertEqual(getattr(self._mlist, key), value)
def test_member_hold(self):
- self._pckdict[b'member_moderation_action'] = 0
+ self._pckdict['member_moderation_action'] = 0
self._do_test(dict(default_member_action=Action.hold))
def test_member_reject(self):
- self._pckdict[b'member_moderation_action'] = 1
+ self._pckdict['member_moderation_action'] = 1
self._do_test(dict(default_member_action=Action.reject))
def test_member_discard(self):
- self._pckdict[b'member_moderation_action'] = 2
+ self._pckdict['member_moderation_action'] = 2
self._do_test(dict(default_member_action=Action.discard))
def test_nonmember_accept(self):
- self._pckdict[b'generic_nonmember_action'] = 0
+ self._pckdict['generic_nonmember_action'] = 0
self._do_test(dict(default_nonmember_action=Action.accept))
def test_nonmember_hold(self):
- self._pckdict[b'generic_nonmember_action'] = 1
+ self._pckdict['generic_nonmember_action'] = 1
self._do_test(dict(default_nonmember_action=Action.hold))
def test_nonmember_reject(self):
- self._pckdict[b'generic_nonmember_action'] = 2
+ self._pckdict['generic_nonmember_action'] = 2
self._do_test(dict(default_nonmember_action=Action.reject))
def test_nonmember_discard(self):
- self._pckdict[b'generic_nonmember_action'] = 3
+ self._pckdict['generic_nonmember_action'] = 3
self._do_test(dict(default_nonmember_action=Action.discard))
@@ -524,9 +527,9 @@ class TestConvertToURI(unittest.TestCase):
# if it changed from the default so don't import. We may do more harm
# than good and it's easy to change if needed.
test_value = b'TEST-VALUE'
- for oldvar, newvar in self._conf_mapping.iteritems():
+ for oldvar, newvar in self._conf_mapping.items():
self._mlist.mail_host = 'example.com'
- self._pckdict[b'mail_host'] = b'test.example.com'
+ self._pckdict['mail_host'] = b'test.example.com'
self._pckdict[str(oldvar)] = test_value
old_value = getattr(self._mlist, newvar)
# Suppress warning messages in the test output.
@@ -541,7 +544,7 @@ class TestConvertToURI(unittest.TestCase):
for oldvar in self._conf_mapping:
self._pckdict[str(oldvar)] = b'Ol\xe1!'
import_config_pck(self._mlist, self._pckdict)
- for oldvar, newvar in self._conf_mapping.iteritems():
+ for oldvar, newvar in self._conf_mapping.items():
newattr = getattr(self._mlist, newvar)
text = decorate(self._mlist, newattr)
expected = u'Ol\ufffd!'
@@ -557,7 +560,7 @@ class TestConvertToURI(unittest.TestCase):
makedirs(os.path.dirname(footer_path))
with open(footer_path, 'wb') as fp:
fp.write(footer)
- self._pckdict[b'msg_footer'] = b'NEW-VALUE'
+ self._pckdict['msg_footer'] = b'NEW-VALUE'
import_config_pck(self._mlist, self._pckdict)
text = decorate(self._mlist, self._mlist.footer_uri)
self.assertEqual(text, 'NEW-VALUE')
@@ -609,6 +612,8 @@ class TestRosterImport(unittest.TestCase):
self._usermanager = getUtility(IUserManager)
language_manager = getUtility(ILanguageManager)
for code in self._pckdict['language'].values():
+ if isinstance(code, bytes):
+ code = code.decode('utf-8')
if code not in language_manager.codes:
language_manager.add(code, 'utf-8', code)
@@ -641,11 +646,13 @@ class TestRosterImport(unittest.TestCase):
addr = '%s@example.com' % name
member = self._mlist.members.get_member(addr)
self.assertIsNotNone(member, 'Address %s was not imported' % addr)
- self.assertEqual(member.preferred_language.code,
- self._pckdict['language'][addr])
+ code = self._pckdict['language'][addr]
+ if isinstance(code, bytes):
+ code = code.decode('utf-8')
+ self.assertEqual(member.preferred_language.code, code)
def test_new_language(self):
- self._pckdict[b'language']['anne@example.com'] = b'xx_XX'
+ self._pckdict['language']['anne@example.com'] = b'xx_XX'
try:
import_config_pck(self._mlist, self._pckdict)
except Import21Error as error:
@@ -698,7 +705,7 @@ class TestRosterImport(unittest.TestCase):
user = self._usermanager.get_user(addr)
self.assertIsNotNone(user, 'Address %s was not imported' % addr)
self.assertEqual(
- user.password, b'{plaintext}%spass' % name,
+ user.password, '{plaintext}%spass' % name,
'Password for %s was not imported' % addr)
def test_same_user(self):
@@ -765,7 +772,7 @@ class TestPreferencesImport(unittest.TestCase):
self.assertIsNotNone(user, 'User was not imported')
member = self._mlist.members.get_member('anne@example.com')
self.assertIsNotNone(member, 'Address was not subscribed')
- for exp_name, exp_val in expected.iteritems():
+ for exp_name, exp_val in expected.items():
try:
currentval = getattr(member, exp_name)
except AttributeError:
@@ -831,8 +838,10 @@ class TestPreferencesImport(unittest.TestCase):
def test_multiple_options(self):
# DontReceiveDuplicates & DisableMime & SuppressPasswordReminder
- self._pckdict[b'digest_members'] = self._pckdict[b'members'].copy()
- self._pckdict[b'members'] = dict()
+ # Keys might be Python 2 str/bytes or unicode.
+ members = self._pckdict['members']
+ self._pckdict['digest_members'] = members.copy()
+ self._pckdict['members'] = dict()
self._do_test(296, dict(
receive_list_copy=False,
delivery_mode=DeliveryMode.plaintext_digests,
diff --git a/src/mailman/utilities/tests/test_passwords.py b/src/mailman/utilities/tests/test_passwords.py
index 0dd49cb85..b11a7654b 100644
--- a/src/mailman/utilities/tests/test_passwords.py
+++ b/src/mailman/utilities/tests/test_passwords.py
@@ -17,9 +17,6 @@
"""Testing the password utility."""
-from __future__ import absolute_import, print_function, unicode_literals
-
-__metaclass__ = type
__all__ = [
'TestPasswords',
]
diff --git a/src/mailman/utilities/tests/test_templates.py b/src/mailman/utilities/tests/test_templates.py
index 6507bf8e5..b59d2aa1c 100644
--- a/src/mailman/utilities/tests/test_templates.py
+++ b/src/mailman/utilities/tests/test_templates.py
@@ -17,10 +17,10 @@
"""Testing i18n template search and interpolation."""
-from __future__ import absolute_import, print_function, unicode_literals
-
-__metaclass__ = type
__all__ = [
+ 'TestFind',
+ 'TestMake',
+ 'TestSearchOrder',
]
@@ -29,14 +29,13 @@ import shutil
import tempfile
import unittest
-from pkg_resources import resource_filename
-from zope.component import getUtility
-
from mailman.app.lifecycle import create_list
from mailman.config import config
from mailman.interfaces.languages import ILanguageManager
from mailman.testing.layers import ConfigLayer
from mailman.utilities.i18n import TemplateNotFoundError, find, make, search
+from pkg_resources import resource_filename
+from zope.component import getUtility
@@ -191,14 +190,14 @@ class TestFind(unittest.TestCase):
with open(path, 'w') as fp:
fp.write(text)
self.xxsite = os.path.join(
- self.var_dir, 'templates', 'site', 'xx', 'site.txt')
+ self.var_dir, 'templates', 'site', 'xx', 'site.txt')
write('Site template', self.xxsite)
- self.xxdomain = os.path.join(
- self.var_dir, 'templates',
+ self.xxdomain = os.path.join(
+ self.var_dir, 'templates',
'domains', 'example.com', 'xx', 'domain.txt')
write('Domain template', self.xxdomain)
self.xxlist = os.path.join(
- self.var_dir, 'templates',
+ self.var_dir, 'templates',
'lists', 'test@example.com', 'xx', 'list.txt')
write('List template', self.xxlist)
diff --git a/src/mailman/utilities/tests/test_wrap.py b/src/mailman/utilities/tests/test_wrap.py
index eca6f93be..b9feeed92 100644
--- a/src/mailman/utilities/tests/test_wrap.py
+++ b/src/mailman/utilities/tests/test_wrap.py
@@ -17,10 +17,8 @@
"""Test text wrapping."""
-from __future__ import absolute_import, print_function, unicode_literals
-
-__metaclass__ = type
__all__ = [
+ 'TestWrap',
]
diff --git a/src/mailman/utilities/uid.py b/src/mailman/utilities/uid.py
index 4fe862868..0b41b63c2 100644
--- a/src/mailman/utilities/uid.py
+++ b/src/mailman/utilities/uid.py
@@ -21,9 +21,6 @@ Use these functions to create unique ids rather than inlining calls to hashlib
and whatnot. These are better instrumented for testing purposes.
"""
-from __future__ import absolute_import, print_function, unicode_literals
-
-__metaclass__ = type
__all__ = [
'UniqueIDFactory',
'factory',
@@ -35,7 +32,6 @@ import uuid
import errno
from flufl.lock import Lock
-
from mailman.config import config
from mailman.model.uid import UID
from mailman.testing import layers