1 # Copyright (C) 2001-2016 by the Free Software Foundation, Inc.
3 # This file is part of GNU Mailman.
5 # GNU Mailman is free software: you can redistribute it and/or modify it under
6 # the terms of the GNU General Public License as published by the Free
7 # Software Foundation, either version 3 of the License, or (at your option)
10 # GNU Mailman is distributed in the hope that it will be useful, but WITHOUT
11 # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
12 # FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
15 # You should have received a copy of the GNU General Public License along with
16 # GNU Mailman. If not, see <http://www.gnu.org/licenses/>.
18 """Creation/deletion hooks for the Postfix MTA."""
23 from flufl
.lock
import Lock
24 from mailman
import public
25 from mailman
.config
import config
26 from mailman
.config
.config
import external_configuration
27 from mailman
.interfaces
.listmanager
import IListManager
28 from mailman
.interfaces
.mta
import (
29 IMailTransportAgentAliases
, IMailTransportAgentLifecycle
)
30 from mailman
.utilities
.datetime
import now
31 from operator
import attrgetter
32 from zope
.component
import getUtility
33 from zope
.interface
import implementer
36 log
= logging
.getLogger('mailman.error')
37 ALIASTMPL
= '{0:{2}}lmtp:[{1.mta.lmtp_host}]:{1.mta.lmtp_port}'
42 """Duck-typed list for the `IMailTransportAgentAliases` interface."""
44 def __init__(self
, list_name
, mail_host
):
45 self
.list_name
= list_name
46 self
.mail_host
= mail_host
47 self
.posting_address
= '{}@{}'.format(list_name
, mail_host
)
51 @implementer(IMailTransportAgentLifecycle
)
53 """Connect Mailman to Postfix via LMTP."""
56 # Locate and read the Postfix specific configuration file.
57 mta_config
= external_configuration(config
.mta
.configuration
)
58 self
.postmap_command
= mta_config
.get('postfix', 'postmap_command')
60 def create(self
, mlist
):
61 """See `IMailTransportAgentLifecycle`."""
62 # We can ignore the mlist argument because for LMTP delivery, we just
63 # generate the entire file every time.
68 def regenerate(self
, directory
=None):
69 """See `IMailTransportAgentLifecycle`."""
70 # Acquire a lock file to prevent other processes from racing us here.
72 directory
= config
.DATA_DIR
73 lock_file
= os
.path
.join(config
.LOCK_DIR
, 'mta')
75 lmtp_path
= os
.path
.join(directory
, 'postfix_lmtp')
76 lmtp_path_new
= lmtp_path
+ '.new'
77 with
open(lmtp_path_new
, 'w') as fp
:
78 self
._generate
_lmtp
_file
(fp
)
79 # Atomically rename to the intended path.
80 os
.rename(lmtp_path_new
, lmtp_path
)
81 domains_path
= os
.path
.join(directory
, 'postfix_domains')
82 domains_path_new
= domains_path
+ '.new'
83 with
open(domains_path_new
, 'w') as fp
:
84 self
._generate
_domains
_file
(fp
)
85 # Atomically rename to the intended path.
86 os
.rename(domains_path_new
, domains_path
)
87 # Now, run the postmap command on both newly generated files. If
88 # one files, still try the other one.
90 for path
in (lmtp_path
, domains_path
):
91 command
= self
.postmap_command
+ ' ' + path
92 status
= (os
.system(command
) >> 8) & 0xff
94 msg
= 'command failure: %s, %s, %s'
95 errstr
= os
.strerror(status
)
96 log
.error(msg
, command
, status
, errstr
)
97 errors
.append(msg
% (command
, status
, errstr
))
99 raise RuntimeError(NL
.join(errors
))
101 def _generate_lmtp_file(self
, fp
):
102 # The format for Postfix's LMTP transport map is defined here:
103 # http://www.postfix.org/transport.5.html
105 # Sort all existing mailing list names first by domain, then by
106 # local part. For Postfix we need a dummy entry for the domain.
107 list_manager
= getUtility(IListManager
)
108 utility
= getUtility(IMailTransportAgentAliases
)
110 sort_key
= attrgetter('list_name')
111 for list_name
, mail_host
in list_manager
.name_components
:
112 mlist
= _FakeList(list_name
, mail_host
)
113 by_domain
.setdefault(mlist
.mail_host
, []).append(mlist
)
115 # AUTOMATICALLY GENERATED BY MAILMAN ON {0}
117 # This file is generated by Mailman, and is kept in sync with the binary hash
118 # file. YOU SHOULD NOT MANUALLY EDIT THIS FILE unless you know what you're
119 # doing, and can keep the two files properly in sync. If you screw it up,
120 # you're on your own.
121 """.format(now().replace(microsecond
=0)), file=fp
)
122 for domain
in sorted(by_domain
):
124 # Aliases which are visible only in the @{0} domain.""".format(domain
),
126 for mlist
in sorted(by_domain
[domain
], key
=sort_key
):
127 aliases
= list(utility
.aliases(mlist
))
128 width
= max(len(alias
) for alias
in aliases
) + 3
129 print(ALIASTMPL
.format(aliases
.pop(0), config
, width
), file=fp
)
130 for alias
in aliases
:
131 print(ALIASTMPL
.format(alias
, config
, width
), file=fp
)
134 def _generate_domains_file(self
, fp
):
135 # Uniquify the domains, then sort them alphabetically.
137 for list_name
, mail_host
in getUtility(IListManager
).name_components
:
138 domains
.add(mail_host
)
140 # AUTOMATICALLY GENERATED BY MAILMAN ON {0}
142 # This file is generated by Mailman, and is kept in sync with the binary hash
143 # file. YOU SHOULD NOT MANUALLY EDIT THIS FILE unless you know what you're
144 # doing, and can keep the two files properly in sync. If you screw it up,
145 # you're on your own.
146 """.format(now().replace(microsecond
=0)), file=fp
)
147 for domain
in sorted(domains
):
148 print('{0} {0}'.format(domain
), file=fp
)