1 # Copyright (C) 2000-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/>.
31 from io
import StringIO
32 from mailman
.config
import config
33 from mailman
.core
.runner
import Runner
34 from mailman
.interfaces
.nntp
import NewsgroupModeration
39 log
= logging
.getLogger('mailman.error')
42 # Matches our Mailman crafted Message-IDs. See Utils.unique_message_id()
43 # XXX The move to email.utils.make_msgid() breaks this.
44 mcre
= re
.compile(r
"""
45 <mailman. # match the prefix
47 \d+. # time in seconds since epoch
49 (?P<listname>[^@]+) # list's internal_name()
51 (?P<hostname>[^>]+) # list's mail_host
57 class NNTPRunner(Runner
):
58 def _dispose(self
, mlist
, msg
, msgdata
):
59 # Get NNTP server connection information.
60 host
= config
.nntp
.host
.strip()
61 port
= config
.nntp
.port
.strip()
67 except (TypeError, ValueError):
68 log
.exception('Bad [nntp]port value: {0}'.format(port
))
70 # Make sure we have the most up-to-date state
71 if not msgdata
.get('prepped'):
72 prepare_message(mlist
, msg
, msgdata
)
73 # Flatten the message object, sticking it in a StringIO object
74 fp
= StringIO(msg
.as_string())
77 conn
= nntplib
.NNTP(host
, port
,
79 user
=config
.nntp
.user
,
80 password
=config
.nntp
.password
)
82 except nntplib
.NNTPTemporaryError
:
83 log
.exception('{0} NNTP error for {1}'.format(
84 msg
.get('message-id', 'n/a'), mlist
.fqdn_listname
))
86 log
.exception('{0} NNTP socket error for {1}'.format(
87 msg
.get('message-id', 'n/a'), mlist
.fqdn_listname
))
89 # Some other exception occurred, which we definitely did not
90 # expect, so set this message up for requeuing.
91 log
.exception('{0} NNTP unexpected exception for {1}'.format(
92 msg
.get('message-id', 'n/a'), mlist
.fqdn_listname
))
101 def prepare_message(mlist
, msg
, msgdata
):
102 # If the newsgroup is moderated, we need to add this header for the Usenet
103 # software to accept the posting, and not forward it on to the n.g.'s
104 # moderation address. The posting would not have gotten here if it hadn't
105 # already been approved. 1 == open list, mod n.g., 2 == moderated
106 if mlist
.newsgroup_moderation
in (NewsgroupModeration
.open_moderated
,
107 NewsgroupModeration
.moderated
):
109 msg
['Approved'] = mlist
.posting_address
110 # Should we restore the original, non-prefixed subject for gatewayed
111 # messages? TK: We use stripped_subject (prefix stripped) which was crafted
112 # in the subject-prefix handler to ensure prefix was stripped from the
113 # subject came from mailing list user.
114 stripped_subject
= msgdata
.get('stripped_subject',
115 msgdata
.get('original_subject'))
116 if not mlist
.nntp_prefix_subject_too
and stripped_subject
is not None:
118 msg
['subject'] = stripped_subject
119 # Add the appropriate Newsgroups header. Multiple Newsgroups headers are
120 # generally not allowed so we're not testing for them.
121 header
= msg
.get('newsgroups')
123 msg
['Newsgroups'] = mlist
.linked_newsgroup
125 # See if the Newsgroups: header already contains our linked_newsgroup.
126 # If so, don't add it again. If not, append our linked_newsgroup to
127 # the end of the header list
128 newsgroups
= [value
.strip() for value
in header
.split(COMMA
)]
129 if mlist
.linked_newsgroup
not in newsgroups
:
130 newsgroups
.append(mlist
.linked_newsgroup
)
131 # Subtitute our new header for the old one.
132 del msg
['newsgroups']
133 msg
['Newsgroups'] = COMMASPACE
.join(newsgroups
)
134 # Note: We need to be sure two messages aren't ever sent to the same list
135 # in the same process, since message ids need to be unique. Further, if
136 # messages are crossposted to two gated mailing lists, they must each have
137 # unique message ids or the nntpd will only accept one of them. The
138 # solution here is to substitute any existing message-id that isn't ours
139 # with one of ours, so we need to parse it to be sure we're not looping.
141 # Our Message-ID format is <mailman.secs.pid.listname@hostname>
143 # XXX 2012-03-31 BAW: What we really want to do is try posting the message
144 # to the nntpd first, and only if that fails substitute a unique
145 # Message-ID. The following should get moved out of prepare_message() and
146 # into _dispose() above.
147 msgid
= msg
['message-id']
150 mo
= mcre
.search(msgid
)
152 lname
, hname
= mo
.group('listname', 'hostname')
153 if lname
== mlist
.internal_name() and hname
== mlist
.mail_host
:
156 del msg
['message-id']
157 msg
['Message-ID'] = email
.utils
.make_msgid()
159 if msg
['Lines'] is None:
160 # BAW: is there a better way?
161 count
= len(list(email
.iterators
.body_line_iterator(msg
)))
162 msg
['Lines'] = str(count
)
163 # Massage the message headers by remove some and rewriting others. This
164 # won't completely sanitize the message, but it will eliminate the bulk of
165 # the rejections based on message headers. The NNTP server may still
166 # reject the message because of other problems.
167 for header
in config
.nntp
.remove_headers
.split():
169 dup_headers
= config
.nntp
.rewrite_duplicate_headers
.split()
170 if len(dup_headers
) % 2 != 0:
171 # There are an odd number of headers; ignore the last one.
172 bad_header
= dup_headers
.pop()
173 log
.error('Ignoring odd [nntp]rewrite_duplicate_headers: {0}'.format(
175 dup_headers
.reverse()
177 source
= dup_headers
.pop()
178 target
= dup_headers
.pop()
179 values
= msg
.get_all(source
, [])
181 # We only care about duplicates.
183 # Delete all the original headers.
185 # Put the first value back on the original header.
186 msg
[source
] = values
[0]
187 # And put all the subsequent values on the destination header.
188 for value
in values
[1:]:
190 # Mark this message as prepared in case it has to be requeued.
191 msgdata
['prepped'] = True