Skip to content
GitLab
Explore
Sign in
Primary navigation
Search or go to…
Project
M
mailman-lmtp
Manage
Activity
Members
Labels
Plan
Issues
Issue boards
Milestones
Code
Merge requests
Repository
Branches
Commits
Tags
Repository graph
Compare revisions
Build
Pipelines
Jobs
Pipeline schedules
Artifacts
Deploy
Releases
Model registry
Operate
Environments
Monitor
Incidents
Service Desk
Analyze
Value stream analytics
Contributor analytics
CI/CD analytics
Repository analytics
Model experiments
Help
Help
Support
GitLab documentation
Compare GitLab plans
Community forum
Contribute to GitLab
Provide feedback
Keyboard shortcuts
?
Snippets
Groups
Projects
Show more breadcrumbs
ai3
thirdparty
mailman-lmtp
Commits
d863629b
Commit
d863629b
authored
6 years ago
by
ale
Browse files
Options
Downloads
Patches
Plain Diff
Add LMTP runner
parent
c8eed950
No related branches found
No related tags found
No related merge requests found
Changes
1
Show whitespace changes
Inline
Side-by-side
Showing
1 changed file
Mailman/Queue/LMTPRunner.py
+490
-0
490 additions, 0 deletions
Mailman/Queue/LMTPRunner.py
with
490 additions
and
0 deletions
Mailman/Queue/LMTPRunner.py
0 → 100644
+
490
−
0
View file @
d863629b
# Copyright (C) 2006-2008 by the Free Software Foundation, Inc.
#
# This program 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 2
# of the License, or (at your option) any later version.
#
# This program 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 this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301,
# USA.
"""
Mailman LMTP runner (server).
Most mail servers can be configured to deliver local messages via
'
LMTP
'
[1].
This module is actually an LMTP server rather than a standard queue runner.
The LMTP runner opens a local TCP port and waits for the mail server to
connect to it. The messages it receives over LMTP are very minimally parsed
for sanity and if they look okay, they are accepted and injected into
Mailman
'
s incoming queue for normal processing. If they don
'
t look good, or
are destined for a bogus sub-address, they are rejected right away, hopefully
so that the peer mail server can provide better diagnostics.
[1] RFC 2033 Local Mail Transport Protocol
http://www.faqs.org/rfcs/rfc2033.html
"""
import
os
import
email
import
smtpd
import
logging
import
asyncore
import
asynchat
import
socket
from
Mailman.Message
import
Message
from
Mailman
import
mm_cfg
from
Mailman.Queue.Runner
import
Runner
from
Mailman.Queue.Switchboard
import
Switchboard
from
Mailman.Handlers.Moderate
import
matches_p
from
Mailman
import
Utils
from
Mailman.Utils
import
ValidateEmail
from
Mailman.Utils
import
list_exists
from
Mailman.Utils
import
get_domain
from
Mailman
import
MemberAdaptor
from
Mailman.OldStyleMemberships
import
OldStyleMemberships
from
Mailman
import
MailList
from
email.Utils
import
parseaddr
from
email._parseaddr
import
AddressList
as
_AddressList
# We only care about the listname and the sub-addresses as in listname@ or listname-request@
SUBADDRESS_NAMES
=
(
'
bounces
'
,
'
confirm
'
,
'
join
'
,
'
leave
'
,
'
owner
'
,
'
request
'
,
'
subscribe
'
,
'
unsubscribe
'
,
)
EMPTYSTRING
=
''
NEWLINE
=
'
\n
'
DASH
=
'
-
'
CRLF
=
'
\r\n
'
# These error codes are now obsolete. The LMTP protocol now uses enhanced error codes
#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 = config.LMTP_ERR_550
# Enhanced error codes
EERR_200
=
'
2.0.0
'
EERR_450
=
'
4.5.0 Other or undefined protocol status
'
EERR_511
=
'
5.1.1 Bad destination mailbox address
'
EERR_513
=
'
5.1.3 Bad destination list address syntax
'
EERR_551
=
'
5.5.1 Invalid command
'
EERR_554
=
'
5.5.4 Invalid command arguments
'
EERR_572
=
'
5.7.2 The sender is not authorized to send a message to the intended mailing list
'
# XXX Blech
__version__
=
'
Python LMTP queue runner 1.1
'
def
split_recipient
(
address
):
"""
Split an address into listname, subaddress and domain parts.
For example:
>>>
split_recipient
(
'
mylist@example.com
'
)
(
'
mylist
'
,
None
,
'
example.com
'
)
>>>
split_recipient
(
'
mylist-request@example.com
'
)
(
'
mylist
'
,
'
request
'
,
'
example.com
'
)
:param address: The destination address.
:return: A 3-tuple of the form (list-shortname, subaddress, domain).
subaddress may be None if this is the list
'
s posting address.
"""
localpart
,
domain
=
address
.
split
(
'
@
'
,
1
)
localpart
=
localpart
.
split
(
'
+
'
,
1
)[
0
]
parts
=
localpart
.
split
(
DASH
)
if
parts
[
-
1
]
in
SUBADDRESS_NAMES
:
listname
=
DASH
.
join
(
parts
[:
-
1
])
subaddress
=
parts
[
-
1
]
else
:
listname
=
localpart
subaddress
=
None
return
listname
,
subaddress
,
domain
class
Channel
(
asynchat
.
async_chat
):
"""
An LMTP channel.
"""
# The LMTP channel is not dependent on the SMTP channel found in Python smtpd,
# It is a complete working LMTP channel, based on smtpd in Python
COMMAND
=
0
DATA
=
1
# The LHLO boolean determines if the LHLO command has been used or not during a session
# False = LHLO has not been used
# True = LHLO has been used
LHLO
=
False
# Don't forget to add:
# QRUNNERS.extend([('LMTPRunner', 1),])
# to mm_cfg so that the LMTP queue runner gets started with the other queue runners
def
__init__
(
self
,
server
,
conn
,
addr
):
asynchat
.
async_chat
.
__init__
(
self
,
conn
)
self
.
_server
=
server
self
.
_conn
=
conn
self
.
_addr
=
addr
self
.
_line
=
[]
self
.
_state
=
self
.
COMMAND
self
.
_greeting
=
0
self
.
_mailfrom
=
None
self
.
_rcpttos
=
[]
self
.
_data
=
''
self
.
_fqdn
=
socket
.
getfqdn
()
self
.
_peer
=
conn
.
getpeername
()
self
.
set_terminator
(
'
\r\n
'
)
self
.
push
(
'
220 %s %s
'
%
(
self
.
_fqdn
,
__version__
))
# smtp_HELO pushs an error if the HELO command is used
def
smtp_HELO
(
self
,
arg
):
self
.
push
(
'
501
'
+
EERR_551
+
'
Use: LHLO command
'
)
return
# smtp_EHLO pushs an error if the EHLO command is used
def
smtp_EHLO
(
self
,
arg
):
self
.
push
(
'
501
'
+
EERR_551
+
'
Use: LHLO command
'
)
return
def
smtp_LHLO
(
self
,
arg
):
"""
HELO is not a valid LMTP command.
"""
if
not
arg
:
self
.
push
(
'
501
'
+
EERR_554
+
'
Syntax: lhlo hostname
'
)
return
if
self
.
_greeting
:
self
.
push
(
'
503
'
+
EERR_551
+
'
Duplicate LHLO
'
)
else
:
self
.
LHLO
=
True
self
.
_greeting
=
arg
# Don't forget '-' after the status code on each line
# except for the last line, if there is a multiline response
self
.
push
(
'
250-%s
'
%
self
.
_fqdn
)
# Use following line when Pipelining is supported
#self.push('250-PIPELINING')
self
.
push
(
'
250 ENHANCEDSTATUSCODES
'
)
def
smtp_MAIL
(
self
,
arg
):
# RFC 2033 requires the client to say LHLO to the server before mail can be sent
if
self
.
LHLO
==
False
:
self
.
push
(
'
503
'
+
EERR_551
+
'
Need LHLO command
'
)
return
address
=
self
.
_getaddr
(
'
FROM:
'
,
arg
)
if
arg
else
None
if
not
address
:
self
.
push
(
'
501
'
+
EERR_554
+
'
Syntax: MAIL FROM:<address>
'
)
return
if
self
.
_mailfrom
:
self
.
push
(
'
503
'
+
EERR_551
+
'
Nested MAIL command
'
)
return
self
.
_mailfrom
=
address
self
.
push
(
'
250
'
+
EERR_200
+
'
Ok Sender address accepted
'
)
def
smtp_RCPT
(
self
,
arg
):
if
not
self
.
_mailfrom
:
self
.
push
(
'
503
'
+
EERR_551
+
'
Need MAIL command
'
)
return
address
=
self
.
_getaddr
(
'
TO:
'
,
arg
)
if
arg
else
None
if
not
address
:
self
.
push
(
'
501
'
+
EERR_554
+
'
Syntax: RCPT TO:<address>
'
)
return
# Call rcpttocheck to check if list address has syntax errors
if
self
.
rcpttocheck
(
address
)
==
'
EERR_513
'
:
self
.
push
(
'
550
'
+
EERR_513
+
'
Syntax: list@domain
'
)
return
# Call rcpttocheck to check if list address is a known address.
if
self
.
rcpttocheck
(
address
)
==
'
EERR_511
'
:
self
.
push
(
'
550
'
+
EERR_511
+
'
:
'
+
address
)
return
# get subaddress and listname
listname
=
self
.
mlistname
(
address
)
subaddress
=
self
.
subaddress
(
address
)
# Check if sender is authorised to post to list
if
not
subaddress
in
SUBADDRESS_NAMES
:
if
self
.
listmembercheck
(
self
.
_mailfrom
,
listname
)
==
'
EERR_572
'
:
self
.
push
(
'
550
'
+
EERR_572
+
'
:
'
+
address
)
return
if
subaddress
in
SUBADDRESS_NAMES
:
if
self
.
listmembercheck
(
self
.
_mailfrom
,
listname
)
==
'
EERR_572
'
:
if
subaddress
==
'
leave
'
or
subaddress
==
'
unsubscribe
'
:
self
.
push
(
'
550
'
+
EERR_572
+
'
, the subaddresses -leave and -unsubscribe can not be used by unauthorised senders
'
)
return
self
.
_rcpttos
.
append
(
address
)
self
.
push
(
'
250
'
+
EERR_200
+
'
Ok Recipient address accepted
'
)
def
smtp_DATA
(
self
,
arg
):
if
not
self
.
_rcpttos
:
self
.
push
(
'
503
'
+
EERR_551
+
'
Need a valid recipient
'
)
return
if
arg
:
self
.
push
(
'
501
'
+
EERR_554
+
'
Syntax: DATA
'
)
return
self
.
_state
=
self
.
DATA
self
.
set_terminator
(
'
\r\n
.
\r\n
'
)
self
.
push
(
'
354
'
+
EERR_200
+
'
End data with <CR><LF>.<CR><LF>
'
)
def
smtp_RSET
(
self
,
arg
):
if
arg
:
self
.
push
(
'
501
'
+
EERR_554
+
'
Syntax: RSET
'
)
return
# Resets the sender, recipients, and data, but not the greeting
self
.
_mailfrom
=
None
self
.
_rcpttos
=
[]
self
.
_data
=
''
self
.
_state
=
self
.
COMMAND
self
.
push
(
'
250
'
+
EERR_200
+
'
Ok Reset
'
)
def
smtp_NOOP
(
self
,
arg
):
if
arg
:
self
.
push
(
'
501
'
+
EERR_554
+
'
Syntax: NOOP
'
)
else
:
self
.
push
(
'
250
'
+
EERR_200
+
'
Ok
'
)
def
smtp_QUIT
(
self
,
arg
):
# args is ignored
self
.
push
(
'
221
'
+
EERR_200
+
'
Goodbye
'
)
self
.
close_when_done
()
# Overrides base class for convenience
def
push
(
self
,
msg
):
asynchat
.
async_chat
.
push
(
self
,
msg
+
'
\r\n
'
)
# Implementation of base class abstract method
def
collect_incoming_data
(
self
,
data
):
self
.
_line
.
append
(
data
)
# factored
def
_getaddr
(
self
,
keyword
,
arg
):
address
=
None
keylen
=
len
(
keyword
)
if
arg
[:
keylen
].
upper
()
==
keyword
:
address
=
arg
[
keylen
:].
strip
()
if
not
address
:
pass
elif
address
[
0
]
==
'
<
'
and
address
[
-
1
]
==
'
>
'
and
address
!=
'
<>
'
:
# Addresses can be in the form <person@dom.com> but watch out
# for null address, e.g. <>
address
=
address
[
1
:
-
1
]
return
address
# Implementation of base class abstract method
def
found_terminator
(
self
):
line
=
EMPTYSTRING
.
join
(
self
.
_line
)
self
.
_line
=
[]
if
self
.
_state
==
self
.
COMMAND
:
if
not
line
:
self
.
push
(
'
500
'
+
EERR_551
+
'
Bad syntax
'
)
return
method
=
None
i
=
line
.
find
(
'
'
)
if
i
<
0
:
command
=
line
.
upper
()
arg
=
None
else
:
command
=
line
[:
i
].
upper
()
arg
=
line
[
i
+
1
:].
strip
()
method
=
getattr
(
self
,
'
smtp_
'
+
command
,
None
)
if
not
method
:
self
.
push
(
'
500
'
+
EERR_551
+
'
Command
"
%s
"
not implemented
'
%
command
)
return
method
(
arg
)
return
else
:
if
self
.
_state
!=
self
.
DATA
:
self
.
push
(
'
451
'
+
EERR_450
+
'
Internal confusion
'
)
return
# Remove extraneous carriage returns and de-transparency according
# to RFC 821, Section 4.5.2.
data
=
[]
for
text
in
line
.
split
(
'
\r\n
'
):
if
text
and
text
[
0
]
==
'
.
'
:
data
.
append
(
text
[
1
:])
else
:
data
.
append
(
text
)
self
.
_data
=
NEWLINE
.
join
(
data
)
status
=
self
.
_server
.
process_message
(
self
.
_peer
,
self
.
_mailfrom
,
self
.
_rcpttos
,
self
.
_data
)
self
.
_rcpttos
=
[]
self
.
_mailfrom
=
None
self
.
_state
=
self
.
COMMAND
self
.
set_terminator
(
'
\r\n
'
)
if
not
status
:
self
.
push
(
'
250
'
+
EERR_200
+
'
Ok
'
)
else
:
self
.
push
(
status
)
# listname parses the given address and returns the name of the list
def
mlistname
(
self
,
to
):
try
:
to
=
parseaddr
(
to
)[
1
].
lower
()
listname
,
subaddress
,
domain
=
split_recipient
(
to
)
return
listname
except
:
return
'
Unknown Error
'
# subaddress parses the given address and returns the sub-address of the list
def
subaddress
(
self
,
to
):
try
:
to
=
parseaddr
(
to
)[
1
].
lower
()
listname
,
subaddress
,
domain
=
split_recipient
(
to
)
return
subaddress
except
:
return
'
Unknown Error
'
# domain parses the given address and returns the domain of the list
def
domain
(
self
,
to
):
try
:
to
=
parseaddr
(
to
)[
1
].
lower
()
listname
,
subaddress
,
domain
=
split_recipient
(
to
)
return
domain
except
:
return
'
Unknown Error
'
# rcpttocheck checks if list is known
def
rcpttocheck
(
self
,
to
):
try
:
if
'
@
'
not
in
to
:
return
'
EERR_513
'
name
=
self
.
mlistname
(
to
)
domain
=
self
.
domain
(
to
)
sitedomain
=
get_domain
()
if
domain
==
sitedomain
:
if
list_exists
(
name
):
return
else
:
return
'
EERR_511
'
except
:
return
'
Unknown Error rcpt
'
# listmembercheck checks if sender is authorised to post to intended mailing list
def
listmembercheck
(
self
,
mailfrom
,
to
):
try
:
name
=
self
.
mlistname
(
to
)
mlist
=
MailList
.
MailList
(
to
,
lock
=
False
)
if
mlist
.
isMember
(
mailfrom
):
return
True
if
mlist
.
generic_nonmember_action
!=
2
:
return
True
if
matches_p
(
mailfrom
,
mlist
.
accept_these_nonmembers
,
to
):
return
True
else
:
return
'
EERR_572
'
except
:
return
'
Unknown Error
'
class
LMTPRunner
(
Runner
,
smtpd
.
SMTPServer
):
# Only __init__ is called on startup. Asyncore is responsible for later
# connections from the MTA. slice and numslices are ignored and are
# necessary only to satisfy the API.
def
__init__
(
self
,
slice
=
None
,
numslices
=
1
):
# Don't forget to add:
# LMTP_HOST = 'host'
# LMTP_PORT = port
# to mm_cfg so that we know which host and port to use
localaddr
=
mm_cfg
.
LMTP_HOST
,
mm_cfg
.
LMTP_PORT
# Do not call Runner's constructor because there's no QDIR to create
smtpd
.
SMTPServer
.
__init__
(
self
,
localaddr
,
remoteaddr
=
None
)
def
handle_accept
(
self
):
conn
,
addr
=
self
.
accept
()
channel
=
Channel
(
self
,
conn
,
addr
)
def
process_message
(
self
,
peer
,
mailfrom
,
rcpttos
,
data
):
#The following code is no longer in use due to .defects not being implemeted yet.
###########################################################################################################
#try:
# Refresh the list of list names every time we process a message
# since the set of mailing lists could have changed.
# Parse the message data. If there are any defects in the
# message, reject it right away; it's probably spam.
#msg = email.message_from_string(data, Message)
#if msg.defects:
# return ('501 '+EERR_554+'. Message has defects')
#msg['X-MailFrom'] = mailfrom
#except Exception, e:
#config.db.abort()
#return CRLF.join(['451 '+EERR_450+' Requested action aborted: error in processing' for to in rcpttos])
###########################################################################################################
# Need a msg object
# Copied from above section of code
msg
=
email
.
message_from_string
(
data
,
Message
)
# RFC 2033 requires us to return a status code for every recipient.
status
=
[]
# Now for each address in the recipients, parse the address to first
# see if it's destined for a valid mailing list. If so, then queue
# the message to the appropriate place and record a 250 status for
# that recipient. If not, record a failure status for that recipient.
for
to
in
rcpttos
:
try
:
to
=
parseaddr
(
to
)[
1
].
lower
()
listname
,
subaddress
,
domain
=
split_recipient
(
to
)
if
list_exists
(
listname
)
==
False
:
status
.
append
(
'
550
'
+
EERR_511
)
continue
# The recipient is a valid mailing list; see if it's a valid
# sub-address, and if so, enqueue it.
queue
=
None
msgdata
=
dict
(
listname
=
listname
)
if
subaddress
==
'
bounces
'
:
queue
=
Switchboard
(
mm_cfg
.
BOUNCEQUEUE_DIR
)
elif
subaddress
==
'
confirm
'
:
msgdata
[
'
toconfirm
'
]
=
True
queue
=
Switchboard
(
mm_cfg
.
CMDQUEUE_DIR
)
elif
subaddress
in
(
'
join
'
,
'
subscribe
'
):
msgdata
[
'
tojoin
'
]
=
True
queue
=
Switchboard
(
mm_cfg
.
CMDQUEUE_DIR
)
elif
subaddress
in
(
'
leave
'
,
'
unsubscribe
'
):
msgdata
[
'
toleave
'
]
=
True
queue
=
Switchboard
(
mm_cfg
.
CMDQUEUE_DIR
)
elif
subaddress
==
'
owner
'
:
msgdata
.
update
(
dict
(
toowner
=
True
,
# SITE_OWNER_ADDRESS does not exist in Defaults.py
# Add it to mm_cfg
envsender
=
mm_cfg
.
SITE_OWNER_ADDRESS
,
pipeline
=
mm_cfg
.
OWNER_PIPELINE
,
))
queue
=
Switchboard
(
mm_cfg
.
INQUEUE_DIR
)
elif
subaddress
is
None
:
msgdata
[
'
tolist
'
]
=
True
queue
=
Switchboard
(
mm_cfg
.
INQUEUE_DIR
)
elif
subaddress
==
'
request
'
:
msgdata
[
'
torequest
'
]
=
True
queue
=
Switchboard
(
mm_cfg
.
CMDQUEUE_DIR
)
else
:
status
.
append
(
'
550
'
+
EERR_511
)
continue
# If we found a valid subaddress, enqueue the message and add
# a success status for this recipient.
if
queue
is
not
None
:
queue
.
enqueue
(
msg
,
msgdata
)
status
.
append
(
'
250
'
+
EERR_200
+
'
Ok Message enqueued for
'
+
to
)
except
Exception
,
e
:
status
.
append
(
'
550
'
+
EERR_513
)
# All done; returning this big status string should give the expected
# response to the LMTP client.
return
CRLF
.
join
(
status
)
def
run
(
self
):
"""
See `IRunner`.
"""
asyncore
.
loop
()
def
stop
(
self
):
"""
See `IRunner`.
"""
asyncore
.
socket_map
.
clear
()
asyncore
.
close_all
()
self
.
close
()
This diff is collapsed.
Click to expand it.
Preview
0%
Loading
Try again
or
attach a new file
.
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Save comment
Cancel
Please
register
or
sign in
to comment