Skip to content
Projects
Groups
Snippets
Help
Loading...
Help
Support
Keyboard shortcuts
?
Submit feedback
Contribute to GitLab
Sign in / Register
Toggle navigation
Z
Zope
Project overview
Project overview
Details
Activity
Releases
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Issues
0
Issues
0
List
Boards
Labels
Milestones
Merge Requests
0
Merge Requests
0
Analytics
Analytics
Repository
Value Stream
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Create a new issue
Commits
Issue Boards
Open sidebar
Kirill Smelkov
Zope
Commits
d8404fba
Commit
d8404fba
authored
Sep 16, 2009
by
Alec Mitchell
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
Merge MailHost changes from trunk (r102756:102819)
parent
6075e345
Changes
5
Expand all
Hide whitespace changes
Inline
Side-by-side
Showing
5 changed files
with
493 additions
and
65 deletions
+493
-65
doc/CHANGES.rst
doc/CHANGES.rst
+5
-0
src/Products/MailHost/MailHost.py
src/Products/MailHost/MailHost.py
+152
-46
src/Products/MailHost/README.txt
src/Products/MailHost/README.txt
+7
-1
src/Products/MailHost/interfaces.py
src/Products/MailHost/interfaces.py
+2
-1
src/Products/MailHost/tests/testMailHost.py
src/Products/MailHost/tests/testMailHost.py
+327
-17
No files found.
doc/CHANGES.rst
View file @
d8404fba
...
@@ -49,6 +49,11 @@ Zope 2.12.0 b4 (2008/08/06)
...
@@ -49,6 +49,11 @@ Zope 2.12.0 b4 (2008/08/06)
Features Added
Features Added
++++++++++++++
++++++++++++++
- The send method of MailHost now supports unicode messages and
email.Message.Message objects. It also now accepts charset and
msg_type parameters to help with character, header and body
encoding.
- Updated packages:
- Updated packages:
- ZODB3 = 3.9.0b5
- ZODB3 = 3.9.0b5
...
...
src/Products/MailHost/MailHost.py
View file @
d8404fba
...
@@ -14,10 +14,24 @@
...
@@ -14,10 +14,24 @@
$Id$
$Id$
"""
"""
from
cStringIO
import
StringIO
import
logging
import
logging
import
mimetools
import
re
import
rfc822
from
cStringIO
import
StringIO
from
copy
import
deepcopy
from
email.Header
import
Header
from
email.Charset
import
Charset
from
email
import
message_from_string
from
email.Message
import
Message
from
email
import
Encoders
try
:
import
email.utils
as
emailutils
except
ImportError
:
import
email.Utils
as
emailutils
import
email.Charset
# We import from a private module here because the email module
# doesn't provide a good public address list parser
import
uu
from
threading
import
Lock
from
threading
import
Lock
import
time
import
time
...
@@ -49,6 +63,13 @@ queue_threads = {} # maps MailHost path -> queue processor threada
...
@@ -49,6 +63,13 @@ queue_threads = {} # maps MailHost path -> queue processor threada
LOG
=
logging
.
getLogger
(
'MailHost'
)
LOG
=
logging
.
getLogger
(
'MailHost'
)
# Encode utf-8 emails as Quoted Printable by default
email
.
Charset
.
add_charset
(
"utf-8"
,
email
.
Charset
.
QP
,
email
.
Charset
.
QP
,
"utf-8"
)
formataddr
=
emailutils
.
formataddr
parseaddr
=
emailutils
.
parseaddr
getaddresses
=
emailutils
.
getaddresses
CHARSET_RE
=
re
.
compile
(
'charset=[
\
'
"]?([
\
w-]+)[
\
'"]?', re.IGNORECASE)
class MailHostError(Exception):
class MailHostError(Exception):
pass
pass
...
@@ -91,7 +112,6 @@ class MailBase(Implicit, Item, RoleManager):
...
@@ -91,7 +112,6 @@ class MailBase(Implicit, Item, RoleManager):
lock = Lock()
lock = Lock()
# timeout = 1.0 # unused?
# timeout = 1.0 # unused?
manage_options = (
manage_options = (
(
(
...
@@ -185,18 +205,19 @@ class MailBase(Implicit, Item, RoleManager):
...
@@ -185,18 +205,19 @@ class MailBase(Implicit, Item, RoleManager):
encode=None,
encode=None,
REQUEST=None,
REQUEST=None,
immediate=False,
immediate=False,
charset=None,
msg_type=None,
):
):
"""Render a mail template, then send it...
"""Render a mail template, then send it...
"""
"""
mtemplate = getattr(self, messageTemplate)
mtemplate = getattr(self, messageTemplate)
messageText = mtemplate(self, trueself.REQUEST)
messageText = mtemplate(self, trueself.REQUEST)
messageText
,
mto
,
mfrom
=
_mungeHeaders
(
messageText
,
mto
,
mfrom
)
trueself.send(messageText, mto=mto, mfrom=mfrom,
messageText
=
_encode
(
messageText
,
encode
)
encode=encode, immediate=immediate,
trueself
.
_send
(
mfrom
,
mto
,
messageText
,
immediat
e
)
charset=charset, msg_type=msg_typ
e)
if
not
statusTemplate
:
if not statusTemplate:
return "
SEND
OK
"
return "
SEND
OK
"
try:
try:
stemplate = getattr(self, statusTemplate)
stemplate = getattr(self, statusTemplate)
return stemplate(self, trueself.REQUEST)
return stemplate(self, trueself.REQUEST)
...
@@ -211,10 +232,15 @@ class MailBase(Implicit, Item, RoleManager):
...
@@ -211,10 +232,15 @@ class MailBase(Implicit, Item, RoleManager):
subject=None,
subject=None,
encode=None,
encode=None,
immediate=False,
immediate=False,
charset=None,
msg_type=None,
):
):
messageText, mto, mfrom = _mungeHeaders(messageText, mto, mfrom,
messageText
,
mto
,
mfrom
=
_mungeHeaders
(
messageText
,
subject, charset, msg_type)
mto
,
mfrom
,
subject
)
# This encode step is mainly for BBB, encoding should be
# automatic if charset is passed. The automated charset-based
# encoding will be preferred if both encode and charset are
# provided.
messageText = _encode(messageText, encode)
messageText = _encode(messageText, encode)
self._send(mfrom, mto, messageText, immediate)
self._send(mfrom, mto, messageText, immediate)
...
@@ -327,68 +353,148 @@ InitializeClass(MailBase)
...
@@ -327,68 +353,148 @@ InitializeClass(MailBase)
class MailHost(Persistent, MailBase):
class MailHost(Persistent, MailBase):
"""persistent version"""
"""persistent version"""
def uu_encoder(msg):
"""For BBB only, don't send uuencoded emails"""
orig = StringIO(msg.get_payload())
encdata = StringIO()
uu.encode(orig, encdata)
msg.set_payload(encdata.getvalue())
# All encodings supported by mimetools for BBB
ENCODERS = {
'base64': Encoders.encode_base64,
'quoted-printable': Encoders.encode_quopri,
'7bit': Encoders.encode_7or8bit,
'8bit': Encoders.encode_7or8bit,
'x-uuencode': uu_encoder,
'uuencode': uu_encoder,
'x-uue': uu_encoder,
'uue': uu_encoder,
}
def _encode(body, encode=None):
def _encode(body, encode=None):
"""Manually sets an encoding and encodes the message if not
already encoded."""
if encode is None:
if encode is None:
return body
return body
mfile
=
StringIO
(
body
)
mo = message_from_string(body)
mo
=
mimetools
.
Message
(
mfile
)
current_coding = mo['Content-Transfer-Encoding']
if
mo
.
getencoding
()
!=
'7bit'
:
if current_coding == encode:
# already encoded correctly, may have been automated
return body
if mo['Content-Transfer-Encoding'] not in ['7bit', None]:
raise MailHostError, 'Message already encoded'
raise MailHostError, 'Message already encoded'
newmfile
=
StringIO
()
if encode in ENCODERS:
newmfile
.
write
(
''
.
join
(
mo
.
headers
)
)
ENCODERS[encode](mo
)
newmfile
.
write
(
'Content-Transfer-Encoding: %s
\
n
'
%
encode
)
if not mo['Content-Transfer-Encoding']:
if
not
mo
.
has_key
(
'Mime-Version'
):
mo['Content-Transfer-Encoding'] = encode
newmfile
.
write
(
'Mime-Version: 1.0
\
n
'
)
if not mo['Mime-Version']:
newmfile
.
write
(
'
\
n
'
)
mo['Mime-Version'] = '1.0'
mimetools
.
encode
(
mfile
,
newmfile
,
encode
)
return mo.as_string(
)
return
newmfile
.
getvalue
()
def _mungeHeaders(messageText, mto=None, mfrom=None, subject=None,
def
_mungeHeaders
(
messageText
,
mto
=
None
,
mfrom
=
None
,
subject
=
None
):
charset=None, msg_type
=None):
"""Sets missing message headers, and deletes Bcc.
"""Sets missing message headers, and deletes Bcc.
returns fixed message, fixed mto and fixed mfrom"""
returns fixed message, fixed mto and fixed mfrom"""
mfile
=
StringIO
(
messageText
.
lstrip
())
# If we have been given unicode fields, attempt to encode them
mo
=
rfc822
.
Message
(
mfile
)
if isinstance(messageText, unicode):
messageText = _try_encode(messageText, charset)
if isinstance(mto, unicode):
mto = _try_encode(mto, charset)
if isinstance(mfrom, unicode):
mfrom = _try_encode(mfrom, charset)
if isinstance(subject, unicode):
subject = _try_encode(subject, charset)
if isinstance(messageText, Message):
# We already have a message, make a copy to operate on
mo = deepcopy(messageText)
else:
# Otherwise parse the input message
mo = message_from_string(messageText)
if msg_type and not mo.get('Content-Type'):
# we don't use get_content_type because that has a default
# value of 'text/plain'
mo.set_type(msg_type)
charset_match = CHARSET_RE.search(mo['Content-Type'] or '')
if charset and not charset_match:
# Don't change the charset if already set
# This encodes the payload automatically based on the default
# encoding for the charset
mo.set_charset(charset)
elif charset_match and not charset:
# If a charset parameter was provided use it for header encoding below,
# Otherwise, try to use the charset provided in the message.
charset = charset_match.groups()[0]
# Parameters given will *always* override headers in the messageText.
# Parameters given will *always* override headers in the messageText.
# This is so that you can't override or add to subscribers by adding
# This is so that you can't override or add to subscribers by adding
# them to # the message text.
# them to # the message text.
if subject:
if subject:
mo
[
'Subject'
]
=
subject
# remove any existing header otherwise we get two
elif
not
mo
.
getheader
(
'Subject'
):
del mo['Subject']
# Perhaps we should ignore errors here and pass 8bit strings
# on encoding errors
mo['Subject'] = Header(subject, charset, errors='replace')
elif not mo.get('Subject'):
mo['Subject'] = '[No Subject]'
mo['Subject'] = '[No Subject]'
if mto:
if mto:
if isinstance(mto, basestring):
if isinstance(mto, basestring):
mto
=
[
rfc822
.
dump_address_pair
(
addr
)
mto = [
formataddr(addr) for addr in getaddresses((mto,))]
for
addr
in
rfc822
.
AddressList
(
mto
)
]
if not mo.get('To'):
if
not
mo
.
getheader
(
'To'
):
mo['To'] = ', '.join(str(_encode_address_string(e, charset))
mo
[
'To'
]
=
','
.
join
(
mto
)
for e in
mto)
else:
else:
# If we don't have recipients, extract them from the message
mto = []
mto = []
for header in ('To', 'Cc', 'Bcc'):
for header in ('To', 'Cc', 'Bcc'):
v
=
mo
.
getheader
(
header
)
v =
','.join(mo.get_all(header) or []
)
if v:
if v:
mto
+=
[
rfc822
.
dump_address_pair
(
addr
)
mto += [formataddr(addr) for addr in getaddresses((v,))]
for
addr
in
rfc822
.
AddressList
(
v
)]
if not mto:
if not mto:
raise MailHostError, "
No
message
recipients
designated
"
raise MailHostError, "
No
message
recipients
designated
"
if mfrom:
if mfrom:
mo
[
'From'
]
=
mfrom
# XXX: do we really want to override an explicitly set From
# header in the messageText
del mo['From']
mo['From'] = _encode_address_string(mfrom, charset)
else:
else:
if
mo
.
get
header
(
'From'
)
is
None
:
if mo.get('From') is None:
raise MailHostError,"
Message
missing
SMTP
Header
'From'"
raise MailHostError,"
Message
missing
SMTP
Header
'From'"
mfrom = mo['From']
mfrom = mo['From']
if
mo
.
get
header
(
'Bcc'
):
if mo.get('Bcc'):
mo
.
__delitem__
(
'Bcc'
)
del mo['Bcc']
if
not
mo
.
get
header
(
'Date'
):
if not mo.get('Date'):
mo['Date'] = DateTime().rfc822()
mo['Date'] = DateTime().rfc822()
mo
.
rewindbody
()
return mo.as_string(), mto, mfrom
finalmessage
=
mo
finalmessage
=
mo
.
__str__
()
+
'
\
n
'
+
mfile
.
read
()
def _try_encode(text, charset):
mfile
.
close
()
"""Attempt to encode using the default charset if none is
return
finalmessage
,
mto
,
mfrom
provided. Should we permit encoding errors?"""
if charset:
return text.encode(charset)
else:
return text.encode()
def _encode_address_string(text, charset):
"""Split the email into parts and use header encoding on the name
part if needed. We do this because the actual addresses need to be
ASCII with no encoding for most SMTP servers, but the non-address
parts should be encoded appropriately."""
header = Header()
name, addr = parseaddr(text)
try:
name.decode('us-ascii')
except UnicodeDecodeError:
if charset:
charset = Charset(charset)
name = charset.header_encode(name)
# We again replace rather than raise an error or pass an 8bit string
header.append(formataddr((name, addr)), errors='replace')
return header
src/Products/MailHost/README.txt
View file @
d8404fba
...
@@ -4,8 +4,14 @@ MailHost
...
@@ -4,8 +4,14 @@ MailHost
The MailHost product provides support for sending email from
The MailHost product provides support for sending email from
within the Zope environment using MailHost objects.
within the Zope environment using MailHost objects.
An optional character set can be specified to automatically encode unicode
input, and perform appropriate RFC 2822 header and body encoding for
the specified character set. Full python email.Message.Message objects
may be sent.
Email can optionally be encoded using Base64, Quoted-Printable
Email can optionally be encoded using Base64, Quoted-Printable
or UUEncode encoding.
or UUEncode encoding (though automatic body encoding will be applied if a
character set is specified).
MailHost provides integration with the Zope transaction system and optional
MailHost provides integration with the Zope transaction system and optional
support for asynchronous mail delivery. Asynchronous mail delivery is
support for asynchronous mail delivery. Asynchronous mail delivery is
...
...
src/Products/MailHost/interfaces.py
View file @
d8404fba
...
@@ -20,6 +20,7 @@ from zope.interface import Interface
...
@@ -20,6 +20,7 @@ from zope.interface import Interface
class
IMailHost
(
Interface
):
class
IMailHost
(
Interface
):
def
send
(
messageText
,
mto
=
None
,
mfrom
=
None
,
subject
=
None
,
encode
=
None
):
def
send
(
messageText
,
mto
=
None
,
mfrom
=
None
,
subject
=
None
,
encode
=
None
,
charset
=
None
,
msg_type
=
None
):
"""Send mail.
"""Send mail.
"""
"""
src/Products/MailHost/tests/testMailHost.py
View file @
d8404fba
This diff is collapsed.
Click to expand it.
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment