1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
|
# Copyright (C) 1998-2016 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/>.
"""Standard Mailman message object.
This is a subclass of email.message.Message but provides a slightly extended
interface which is more convenient for use inside Mailman. It also supports
safe pickle deserialization, even if the email package adds additional Message
attributes.
"""
import email
import email.message
import email.utils
from email.header import Header
from email.mime.multipart import MIMEMultipart
from mailman import public
from mailman.config import config
from mailman.interfaces.member import DeliveryStatus
COMMASPACE = ', '
@public
class Message(email.message.Message):
# BAW: For debugging w/ bin/dumpdb. Apparently pprint uses repr.
def __repr__(self):
return self.__str__()
def __setstate__(self, values):
self.__dict__ = values
@property
def sender(self):
"""The address considered to be the author of the email.
This is the first non-None value in the list of senders.
:return: The email address of the first found sender, or the empty
string if no sender address was found.
:rtype: email address
"""
for address in self.senders:
# This could be None or the empty string.
if address:
return address
return ''
@property
def senders(self):
"""Return a list of addresses representing the author of the email.
The list will contain email addresses in the order determined by the
configuration variable `sender_headers` in the `[mailman]` section.
By default it uses this list of headers in order:
1. From:
2. envelope sender (i.e. From_, unixfrom, or RFC 2821 MAIL FROM)
3. Reply-To:
4. Sender:
The return addresses are guaranteed to be lower case or None. There
may be more than four values in the returned list, since some of the
originator headers above can appear multiple times in the message, or
contain multiple values.
:return: The list of email addresses that can be considered the sender
of the message.
:rtype: A list of email addresses or Nones
"""
envelope_sender = self.get_unixfrom()
senders = []
for header in config.mailman.sender_headers.split():
header = header.lower()
if header == 'from_':
senders.append(envelope_sender.lower()
if envelope_sender is not None
else '')
else:
field_values = self.get_all(header, [])
senders.extend(address.lower() for (display_name, address)
in email.utils.getaddresses(field_values))
# Filter out None and the empty string, and convert to unicode.
clean_senders = []
for sender in senders:
if not sender:
continue
if isinstance(sender, bytes):
sender = sender.decode('ascii')
clean_senders.append(sender)
return clean_senders
@public
class MultipartDigestMessage(MIMEMultipart, Message):
"""Mix-in class for MIME digest messages."""
@public
class UserNotification(Message):
"""Class for internally crafted messages."""
def __init__(self, recipients, sender, subject=None, text=None, lang=None):
Message.__init__(self)
charset = (lang.charset if lang is not None else 'us-ascii')
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, charset, header_name='Subject', errors='replace')
self['From'] = sender
if isinstance(recipients, (list, set, tuple)):
self['To'] = COMMASPACE.join(recipients)
self.recipients = recipients
else:
self['To'] = recipients
self.recipients = set([recipients])
def send(self, mlist, add_precedence=True, tomoderators=False, **_kws):
"""Sends the message by enqueuing it to the 'virgin' queue.
This is used for all internally crafted messages.
:param mlist: The mailing list to send the message to.
:type mlist: `IMailingList`
:param add_precedence: Flag indicating whether a `Precedence: bulk`
header should be added to the message or not.
:type add_precedence: bool
This function also accepts arbitrary keyword arguments. The key/value
pairs for **kws is added to the metadata dictionary associated with
the enqueued message.
"""
# Since we're crafting the message from whole cloth, let's make sure
# this message has a Message-ID.
if 'message-id' not in self:
self['Message-ID'] = email.utils.make_msgid()
# Ditto for Date: as required by RFC 2822.
if 'date' not in self:
self['Date'] = email.utils.formatdate(localtime=True)
# UserNotifications are typically for admin messages, and for messages
# other than list explosions. Send these out as Precedence: bulk, but
# don't override an existing Precedence: header.
if 'precedence' not in self and add_precedence:
self['Precedence'] = 'bulk'
if tomoderators:
self.recipients = set(
member.address.email
for member in mlist.moderators.members
if member.delivery_status == DeliveryStatus.enabled)
self['To'] = COMMASPACE.join(self.recipients)
self._enqueue(mlist, **_kws)
def _enqueue(self, mlist, **_kws):
# Not imported at module scope to avoid import loop
virginq = config.switchboards['virgin']
# The message metadata better have a 'recip' attribute.
enqueue_kws = dict(
recipients=self.recipients,
nodecorate=True,
reduced_list_headers=True,
)
if mlist is not None:
enqueue_kws['listid'] = mlist.list_id
enqueue_kws.update(_kws)
virginq.enqueue(self, **enqueue_kws)
@public
class OwnerNotification(UserNotification):
"""Like user notifications, but this message goes to some owners."""
def __init__(self, mlist, subject=None, text=None, roster=None):
if roster is None:
recipients = set([config.mailman.site_owner])
to = config.mailman.site_owner
else:
recipients = set(address.email for address in roster.addresses)
to = mlist.owner_address
sender = config.mailman.site_owner
UserNotification.__init__(self, recipients, sender, subject,
text, mlist.preferred_language)
# Hack the To header to look like it's going to the -owner address
del self['to']
self['To'] = to
self._sender = sender
def _enqueue(self, mlist, **_kws):
# Not imported at module scope to avoid import loop
virginq = config.switchboards['virgin']
# The message metadata better have a `recip' attribute
virginq.enqueue(self,
listid=mlist.list_id,
recipients=self.recipients,
nodecorate=True,
reduced_list_headers=True,
envsender=self._sender,
**_kws)
|