1 # Copyright (C) 2000-2023 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 <https://www.gnu.org/licenses/>.
29 from datetime
import datetime
30 from io
import BytesIO
31 from lazr
.config
import as_timedelta
32 from mailman
.config
import config
33 from mailman
.core
.runner
import Runner
34 from mailman
.interfaces
.nntp
import NewsgroupModeration
35 from public
import public
40 log
= logging
.getLogger('mailman.error')
43 # Matches our crafted Message-ID.
44 mcre
= re
.compile(r
"""
46 \d+\. # time in centi-seconds since epoch
49 (?P<listname>[^@]+) # list's list_name
51 (?P<hostname>[^>]+) # list's mail_host
57 class NNTPRunner(Runner
):
58 def __init__(self
, name
, slice=None):
59 super().__init
__(name
, slice)
60 self
.lastrun
= datetime
.min
61 self
.delay
= as_timedelta(config
.nntp
.gatenews_every
)
63 python
= sys
.executable
64 mailman
= os
.path
.join(config
.BIN_DIR
, 'mailman')
65 conf
= config
.filename
66 self
.cmd
= [python
, mailman
, '-C', conf
, 'gatenews']
69 def _dispose(self
, mlist
, msg
, msgdata
):
70 # Get NNTP server connection information.
71 host
= config
.nntp
.host
.strip()
72 port
= config
.nntp
.port
.strip()
78 except (TypeError, ValueError):
79 log
.exception('Bad [nntp]port value: {}'.format(port
))
81 # Make sure we have the most up-to-date state
82 if not msgdata
.get('prepped'):
83 prepare_message(mlist
, msg
, msgdata
)
84 # Flatten the message object, sticking it in a BytesIO object
86 email
.generator
.BytesGenerator(fp
, maxheaderlen
=0).flatten(msg
)
90 conn
= nntplib
.NNTP(host
, port
,
92 user
=config
.nntp
.user
,
93 password
=config
.nntp
.password
)
95 except nntplib
.NNTPTemporaryError
:
96 # This could be a duplicate Message-ID for a message cross-posted
97 # to another group. See if we already munged the Message-ID.
98 mo
= mcre
.search(msg
.get('message-id', 'n/a'))
99 if (mo
and mo
.group('listname') == mlist
.list_name
and
100 mo
.group('hostname') == mlist
.mail_host
):
101 # This is our munged Message-ID. This must be a failure of the
102 # requeued message or a Message-ID we added in prepare_message.
103 # Get the original Message-ID or the one we added and log it.
104 log_message_id
= msgdata
.get('original_message-id',
105 msg
.get('message-id', 'n/a'))
106 log
.exception('{} NNTP error for {}'.format(
107 log_message_id
, mlist
.fqdn_listname
))
109 # This might be a duplicate Message-ID. Munge it and requeue
110 # the message, but save the original Message-ID for logging.
111 msgdata
['original_message-id'] = msg
.get('message-id', 'n/a')
112 del msg
['message-id']
113 msg
['Message-ID'] = email
.utils
.make_msgid(mlist
.list_name
,
117 log
.exception('{} NNTP socket error for {}'.format(
118 msg
.get('message-id', 'n/a'), mlist
.fqdn_listname
))
120 # Some other exception occurred, which we definitely did not
121 # expect, so set this message up for requeuing.
122 log
.exception('{} NNTP unexpected exception for {}'.format(
123 msg
.get('message-id', 'n/a'), mlist
.fqdn_listname
))
130 def _do_periodic(self
):
131 """Invoked periodically by the run() method in the super class."""
132 if self
.lastrun
+ self
.delay
> datetime
.now():
133 return # pragma: nocover
134 if not (self
.slice in (None, 0)):
135 # If queue is sliced, only run for slice = 0.
136 return # pragma: nocover
137 self
.lastrun
= datetime
.now()
138 log
.debug('Running nntp runner periodic task gatenews')
139 os
.environ
['_MAILMAN_GATENEWS_NNTP'] = 'yes'
140 result
= subprocess
.run(self
.cmd
, stderr
=subprocess
.PIPE
)
141 if result
.returncode
!= 0:
143 gatenews failed. status: {result.returncode}
144 message: {result.stderr}""")
147 def prepare_message(mlist
, msg
, msgdata
):
148 # If the newsgroup is moderated, we need to add this header for the Usenet
149 # software to accept the posting, and not forward it on to the n.g.'s
150 # moderation address. The posting would not have gotten here if it hadn't
151 # already been approved. 1 == open list, mod n.g., 2 == moderated
152 if mlist
.newsgroup_moderation
in (NewsgroupModeration
.open_moderated
,
153 NewsgroupModeration
.moderated
):
155 msg
['Approved'] = mlist
.posting_address
156 # Should we restore the original, non-prefixed subject for gatewayed
157 # messages? TK: We use stripped_subject (prefix stripped) which was crafted
158 # in the subject-prefix handler to ensure prefix was stripped from the
159 # subject came from mailing list user.
160 stripped_subject
= msgdata
.get('stripped_subject',
161 msgdata
.get('original_subject'))
162 if not mlist
.nntp_prefix_subject_too
and stripped_subject
is not None:
164 msg
['Subject'] = stripped_subject
165 # Add the appropriate Newsgroups header. Multiple Newsgroups headers are
166 # generally not allowed so we're not testing for them.
167 header
= msg
.get('newsgroups')
169 msg
['Newsgroups'] = mlist
.linked_newsgroup
171 # See if the Newsgroups: header already contains our linked_newsgroup.
172 # If so, don't add it again. If not, append our linked_newsgroup to
173 # the end of the header list
174 newsgroups
= [value
.strip() for value
in header
.split(COMMA
)]
175 if mlist
.linked_newsgroup
not in newsgroups
:
176 newsgroups
.append(mlist
.linked_newsgroup
)
177 # Subtitute our new header for the old one.
178 del msg
['newsgroups']
179 msg
['Newsgroups'] = COMMASPACE
.join(newsgroups
)
180 # Ensure we have an unfolded Message-ID.
181 if not msg
.get('message-id'):
182 msg
['Message-ID'] = email
.utils
.make_msgid(mlist
.list_name
,
184 mid
= re
.sub(r
'[\s]', '', msg
.get('message-id'))
185 msg
.replace_header('message-id', mid
)
187 if msg
['Lines'] is None:
188 # BAW: is there a better way?
189 count
= len(list(email
.iterators
.body_line_iterator(msg
)))
190 msg
['Lines'] = str(count
)
191 # Massage the message headers by remove some and rewriting others. This
192 # won't completely sanitize the message, but it will eliminate the bulk of
193 # the rejections based on message headers. The NNTP server may still
194 # reject the message because of other problems.
195 for header
in config
.nntp
.remove_headers
.split():
197 dup_headers
= config
.nntp
.rewrite_duplicate_headers
.split()
198 if len(dup_headers
) % 2 != 0:
199 # There are an odd number of headers; ignore the last one.
200 bad_header
= dup_headers
.pop()
201 log
.error('Ignoring odd [nntp]rewrite_duplicate_headers: {}'.format(
203 dup_headers
.reverse()
205 source
= dup_headers
.pop()
206 target
= dup_headers
.pop()
207 values
= msg
.get_all(source
, [])
209 # We only care about duplicates.
211 # Delete all the original headers.
213 # Put the first value back on the original header.
214 msg
[source
] = values
[0]
215 # And put all the subsequent values on the destination header.
216 for value
in values
[1:]:
218 # Mark this message as prepared in case it has to be requeued.
219 msgdata
['prepped'] = True