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
c8a66859
Commit
c8a66859
authored
Jul 13, 2010
by
Hanno Schlichting
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
Factored out MailHost
parent
4c48339e
Changes
21
Show whitespace changes
Inline
Side-by-side
Showing
21 changed files
with
6 additions
and
1793 deletions
+6
-1793
buildout.cfg
buildout.cfg
+1
-0
doc/CHANGES.rst
doc/CHANGES.rst
+3
-0
setup.py
setup.py
+1
-0
src/Products/MailHost/MailHost.py
src/Products/MailHost/MailHost.py
+0
-518
src/Products/MailHost/README.txt
src/Products/MailHost/README.txt
+0
-24
src/Products/MailHost/SendMailTag.py
src/Products/MailHost/SendMailTag.py
+0
-122
src/Products/MailHost/__init__.py
src/Products/MailHost/__init__.py
+0
-30
src/Products/MailHost/decorator.py
src/Products/MailHost/decorator.py
+0
-30
src/Products/MailHost/dtml/addMailHost_form.dtml
src/Products/MailHost/dtml/addMailHost_form.dtml
+0
-71
src/Products/MailHost/dtml/manageMailHost.dtml
src/Products/MailHost/dtml/manageMailHost.dtml
+0
-176
src/Products/MailHost/help/Mail-Host.stx
src/Products/MailHost/help/Mail-Host.stx
+0
-45
src/Products/MailHost/help/Mail-Host_Add.stx
src/Products/MailHost/help/Mail-Host_Add.stx
+0
-18
src/Products/MailHost/help/Mail-Host_Edit.stx
src/Products/MailHost/help/Mail-Host_Edit.stx
+0
-18
src/Products/MailHost/help/MailHost.py
src/Products/MailHost/help/MailHost.py
+0
-70
src/Products/MailHost/interfaces.py
src/Products/MailHost/interfaces.py
+0
-26
src/Products/MailHost/mailer.py
src/Products/MailHost/mailer.py
+0
-7
src/Products/MailHost/tests/__init__.py
src/Products/MailHost/tests/__init__.py
+0
-15
src/Products/MailHost/tests/testMailHost.py
src/Products/MailHost/tests/testMailHost.py
+0
-622
src/Products/MailHost/version.txt
src/Products/MailHost/version.txt
+0
-1
src/Products/MailHost/www/MailHost_icon.gif
src/Products/MailHost/www/MailHost_icon.gif
+0
-0
versions.cfg
versions.cfg
+1
-0
No files found.
buildout.cfg
View file @
c8a66859
...
@@ -51,6 +51,7 @@ eggs =
...
@@ -51,6 +51,7 @@ eggs =
Persistence
Persistence
Products.BTreeFolder2
Products.BTreeFolder2
Products.ExternalMethod
Products.ExternalMethod
Products.MailHost
Products.PythonScripts
Products.PythonScripts
Products.StandardCacheManagers
Products.StandardCacheManagers
Products.ZCTextIndex
Products.ZCTextIndex
...
...
doc/CHANGES.rst
View file @
c8a66859
...
@@ -15,6 +15,9 @@ Bugs Fixed
...
@@ -15,6 +15,9 @@ Bugs Fixed
Restructuring
Restructuring
+++++++++++++
+++++++++++++
- Factored out the `Products.MailHost` package into its own distributions. It
will no longer be included by default in Zope 2.14 but live on as an
independent add-on.
Features Added
Features Added
++++++++++++++
++++++++++++++
...
...
setup.py
View file @
c8a66859
...
@@ -101,6 +101,7 @@ setup(name='Zope2',
...
@@ -101,6 +101,7 @@ setup(name='Zope2',
# BBB optional dependencies to be removed in Zope 2.14
# BBB optional dependencies to be removed in Zope 2.14
'Products.BTreeFolder2'
,
'Products.BTreeFolder2'
,
'Products.ExternalMethod'
,
'Products.ExternalMethod'
,
'Products.MailHost'
,
'Products.MIMETools'
,
'Products.MIMETools'
,
'Products.OFSP'
,
'Products.OFSP'
,
'Products.PythonScripts'
,
'Products.PythonScripts'
,
...
...
src/Products/MailHost/MailHost.py
deleted
100644 → 0
View file @
4c48339e
##############################################################################
#
# Copyright (c) 2002 Zope Foundation and Contributors.
#
# This software is subject to the provisions of the Zope Public License,
# Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution.
# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
# FOR A PARTICULAR PURPOSE.
#
##############################################################################
"""SMTP mail objects
$Id$
"""
import
logging
from
os.path
import
realpath
import
re
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
import
time
from
AccessControl.class_init
import
InitializeClass
from
AccessControl.SecurityInfo
import
ClassSecurityInfo
from
AccessControl.Permissions
import
change_configuration
,
view
from
AccessControl.Permissions
import
use_mailhost_services
from
Acquisition
import
Implicit
from
App.special_dtml
import
DTMLFile
from
DateTime.DateTime
import
DateTime
from
Persistence
import
Persistent
from
OFS.role
import
RoleManager
from
OFS.SimpleItem
import
Item
from
zope.interface
import
implements
from
zope.sendmail.mailer
import
SMTPMailer
from
zope.sendmail.maildir
import
Maildir
from
zope.sendmail.delivery
import
DirectMailDelivery
,
QueuedMailDelivery
,
\
QueueProcessorThread
from
interfaces
import
IMailHost
from
decorator
import
synchronized
queue_threads
=
{}
# maps MailHost path -> queue processor threada
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):
pass
manage_addMailHostForm = DTMLFile('dtml/addMailHost_form', globals())
def manage_addMailHost(self,
id,
title='',
smtp_host='localhost',
localhost='localhost',
smtp_port=25,
timeout=1.0,
REQUEST=None,
):
""" Add a MailHost into the system.
"""
i = MailHost( id, title, smtp_host, smtp_port ) #create new mail host
self._setObject( id,i ) #register it
if REQUEST is not None:
REQUEST['RESPONSE'].redirect(self.absolute_url()+'/manage_main')
add = manage_addMailHost
class MailBase(Implicit, Item, RoleManager):
"""a mailhost...?"""
implements(IMailHost)
meta_type = 'Mail Host'
manage = manage_main = DTMLFile('dtml/manageMailHost', globals())
manage_main._setName('manage_main')
index_html = None
security = ClassSecurityInfo()
smtp_uid = '' # Class attributes for smooth upgrades
smtp_pwd = ''
smtp_queue = False
smtp_queue_directory = '/tmp'
force_tls = False
lock = Lock()
# timeout = 1.0 # unused?
manage_options = (
(
{'icon':'', 'label':'Edit',
'action':'manage_main',
'help':('MailHost','Mail-Host_Edit.stx')},
)
+ RoleManager.manage_options
+ Item.manage_options
)
def __init__(self,
id='',
title='',
smtp_host='localhost',
smtp_port=25,
force_tls=False,
smtp_uid='',
smtp_pwd='',
smtp_queue=False,
smtp_queue_directory='/tmp',
):
"""Initialize a new MailHost instance.
"""
self.id = id
self.title = title
self.smtp_host = str( smtp_host )
self.smtp_port = int(smtp_port)
self.smtp_uid = smtp_uid
self.smtp_pwd = smtp_pwd
self.force_tls = force_tls
self.smtp_queue = smtp_queue
self.smtp_queue_directory = smtp_queue_directory
# staying for now... (backwards compatibility)
def _init(self, smtp_host, smtp_port):
self.smtp_host = smtp_host
self.smtp_port = smtp_port
security.declareProtected(change_configuration, 'manage_makeChanges')
def manage_makeChanges(self,
title,
smtp_host,
smtp_port,
smtp_uid='',
smtp_pwd='',
smtp_queue=False,
smtp_queue_directory='/tmp',
force_tls=False,
REQUEST=None,
):
"""Make the changes.
"""
title = str(title)
smtp_host = str(smtp_host)
smtp_port = int(smtp_port)
self.title = title
self.smtp_host = smtp_host
self.smtp_port = smtp_port
self.smtp_uid = smtp_uid
self.smtp_pwd = smtp_pwd
self.force_tls = force_tls
self.smtp_queue = smtp_queue
self.smtp_queue_directory = smtp_queue_directory
# restart queue processor thread
if self.smtp_queue:
self._stopQueueProcessorThread()
self._startQueueProcessorThread()
else:
self._stopQueueProcessorThread()
if REQUEST is not None:
msg = 'MailHost %s updated' % self.id
return self.manage_main( self
, REQUEST
, manage_tabs_message=msg
)
security.declareProtected(use_mailhost_services, 'sendTemplate')
def sendTemplate(trueself,
self,
messageTemplate,
statusTemplate=None,
mto=None,
mfrom=None,
encode=None,
REQUEST=None,
immediate=False,
charset=None,
msg_type=None,
):
"""Render a mail template, then send it...
"""
mtemplate = getattr(self, messageTemplate)
messageText = mtemplate(self, trueself.REQUEST)
trueself.send(messageText, mto=mto, mfrom=mfrom,
encode=encode, immediate=immediate,
charset=charset, msg_type=msg_type)
if not statusTemplate:
return "
SEND
OK
"
try:
stemplate = getattr(self, statusTemplate)
return stemplate(self, trueself.REQUEST)
except:
return "
SEND
OK
"
security.declareProtected(use_mailhost_services, 'send')
def send(self,
messageText,
mto=None,
mfrom=None,
subject=None,
encode=None,
immediate=False,
charset=None,
msg_type=None,
):
messageText, mto, mfrom = _mungeHeaders(messageText, mto, mfrom,
subject, charset, msg_type)
# 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)
self._send(mfrom, mto, messageText, immediate)
# This is here for backwards compatibility only. Possibly it could
# be used to send messages at a scheduled future time, or via a mail queue?
security.declareProtected(use_mailhost_services, 'scheduledSend')
scheduledSend = send
security.declareProtected(use_mailhost_services, 'simple_send')
def simple_send(self, mto, mfrom, subject, body, immediate=False):
body = "
From
:
%
s
\
nTo
:
%
s
\
nSubject
:
%
s
\
n
\
n
%
s
" % (
mfrom, mto, subject, body)
self._send(mfrom, mto, body, immediate)
def _makeMailer(self):
""" Create a SMTPMailer """
return SMTPMailer(hostname=self.smtp_host,
port=int(self.smtp_port),
username=self.smtp_uid or None,
password=self.smtp_pwd or None,
force_tls=self.force_tls
)
security.declarePrivate('_getThreadKey')
def _getThreadKey(self):
""" Return the key used to find our processor thread.
"""
return realpath(self.smtp_queue_directory)
@synchronized(lock)
def _stopQueueProcessorThread(self):
""" Stop thread for processing the mail queue.
"""
key = self._getThreadKey()
if queue_threads.has_key(key):
thread = queue_threads[key]
thread.stop()
while thread.isAlive():
# wait until thread is really dead
time.sleep(0.3)
del queue_threads[key]
LOG.info('Thread for %s stopped' % key)
@synchronized(lock)
def _startQueueProcessorThread(self):
""" Start thread for processing the mail queue.
"""
key = self._getThreadKey()
if not queue_threads.has_key(key):
thread = QueueProcessorThread()
thread.setMailer(self._makeMailer())
thread.setQueuePath(self.smtp_queue_directory)
thread.start()
queue_threads[key] = thread
LOG.info('Thread for %s started' % key)
security.declareProtected(view, 'queueLength')
def queueLength(self):
""" return length of mail queue """
try:
maildir = Maildir(self.smtp_queue_directory)
return len([item for item in maildir])
except ValueError:
return 'n/a - %s is not a maildir - please verify your '
\
'configuration' % self.smtp_queue_directory
security.declareProtected(view, 'queueThreadAlive')
def queueThreadAlive(self):
""" return True/False is queue thread is working
"""
th = queue_threads.get(self._getThreadKey())
if th:
return th.isAlive()
return False
security.declareProtected(change_configuration, 'manage_restartQueueThread')
def manage_restartQueueThread(self, action='start', REQUEST=None):
""" Restart the queue processor thread """
if action == 'stop':
self._stopQueueProcessorThread()
elif action == 'start':
self._startQueueProcessorThread()
else:
raise ValueError('Unsupported action %s' % action)
if REQUEST is not None:
msg = 'Queue processor thread %s' %
\
(action == 'stop' and 'stopped' or 'started')
return self.manage_main(self, REQUEST, manage_tabs_message=msg)
security.declarePrivate('_send')
def _send(self, mfrom, mto, messageText, immediate=False):
""" Send the message """
if immediate:
self._makeMailer().send(mfrom, mto, messageText)
else:
if self.smtp_queue:
# Start queue processor thread, if necessary
self._startQueueProcessorThread()
delivery = QueuedMailDelivery(self.smtp_queue_directory)
else:
delivery = DirectMailDelivery(self._makeMailer())
delivery.send(mfrom, mto, messageText)
InitializeClass(MailBase)
class MailHost(Persistent, MailBase):
"""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):
"""Manually sets an encoding and encodes the message if not
already encoded."""
if encode is None:
return body
mo = message_from_string(body)
current_coding = mo['Content-Transfer-Encoding']
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'
if encode in ENCODERS:
ENCODERS[encode](mo)
if not mo['Content-Transfer-Encoding']:
mo['Content-Transfer-Encoding'] = encode
if not mo['Mime-Version']:
mo['Mime-Version'] = '1.0'
return mo.as_string()
def _mungeHeaders(messageText, mto=None, mfrom=None, subject=None,
charset=None, msg_type=None):
"""Sets missing message headers, and deletes Bcc.
returns fixed message, fixed mto and fixed mfrom"""
# If we have been given unicode fields, attempt to encode them
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)
if not mo.is_multipart():
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]
else:
# Do basically the same for each payload as for the complete
# multipart message.
for index, payload in enumerate(mo.get_payload()):
if not isinstance(payload, Message):
payload = message_from_string(payload)
charset_match = CHARSET_RE.search(payload['Content-Type'] or '')
if payload.get_filename() is None:
# No binary file
if charset and not charset_match:
payload.set_charset(charset)
elif charset_match and not charset:
charset = charset_match.groups()[0]
mo.get_payload()[index] = payload
# Parameters given will *always* override headers in the messageText.
# This is so that you can't override or add to subscribers by adding
# them to # the message text.
if subject:
# remove any existing header otherwise we get two
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]'
if mto:
if isinstance(mto, basestring):
mto = [formataddr(addr) for addr in getaddresses((mto,))]
if not mo.get('To'):
mo['To'] = ', '.join(str(_encode_address_string(e, charset))
for e in mto)
else:
# If we don't have recipients, extract them from the message
mto = []
for header in ('To', 'Cc', 'Bcc'):
v = ','.join(mo.get_all(header) or [])
if v:
mto += [formataddr(addr) for addr in getaddresses((v,))]
if not mto:
raise MailHostError, "
No
message
recipients
designated
"
if 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:
if mo.get('From') is None:
raise MailHostError,"
Message
missing
SMTP
Header
'From'"
mfrom = mo['From']
if mo.get('Bcc'):
del mo['Bcc']
if not mo.get('Date'):
mo['Date'] = DateTime().rfc822()
return mo.as_string(), mto, mfrom
def _try_encode(text, charset):
"""Attempt to encode using the default charset if none is
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
deleted
100644 → 0
View file @
4c48339e
MailHost
The MailHost product provides support for sending email from
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
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
support for asynchronous mail delivery. Asynchronous mail delivery is
implemented using a queue and a dedicated thread processing the queue. The
thread is (re)-started automatically when sending an email. The thread can be
startet manually (in case of restart) by calling its
manage_restartQueueThread?action=start method through HTTP. There is
currently no possibility to start the thread at Zope startup time.
Supports TLS/SSL encryption (requires Python compiled with SSL support)
src/Products/MailHost/SendMailTag.py
deleted
100644 → 0
View file @
4c48339e
##############################################################################
#
# Copyright (c) 2002 Zope Foundation and Contributors.
#
# This software is subject to the provisions of the Zope Public License,
# Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution.
# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
# FOR A PARTICULAR PURPOSE
#
##############################################################################
__rcs_id__
=
'$Id$'
__version__
=
'$Revision: 1.18 $'
[
11
:
-
2
]
from
MailHost
import
MailBase
,
MailHostError
from
DocumentTemplate.DT_Util
import
parse_params
,
render_blocks
from
DocumentTemplate.DT_String
import
String
class
SendMailTag
:
'''the send mail tag, used like thus:
<dtml-sendmail mailhost="someMailHostID">
to: person@their.machine.com
from: me@mymachine.net
subject: just called to say...
boy howdy!
</dtml-sendmail>
Text between the sendmail and /sendmail tags is processed
by the MailHost machinery and delivered. There must be at least
one blank line seperating the headers (to/from/etc..) from the body
of the message.
Instead of specifying a MailHost, an smtphost may be specified
ala 'smtphost="mail.mycompany.com" port=25' (port defaults to 25
automatically). Other parameters are
* mailto -- person (or comma-seperated list of persons) to send the
mail to. If not specified, there **must** be a to: header in the
message.
* mailfrom -- person sending the mail (basically who the recipient can
reply to). If not specified, there **must** be a from: header in the
message.
* subject -- optional subject. If not specified, there **must** be a
subject: header in the message.
* encode -- optional encoding. Possible values are: 'base64',
'quoted-printable' and 'uuencode'.
'''
name
=
'sendmail'
blockContinuations
=
()
encode
=
None
def
__init__
(
self
,
blocks
):
tname
,
args
,
section
=
blocks
[
0
]
args
=
parse_params
(
args
,
mailhost
=
None
,
mailto
=
None
,
mailfrom
=
None
,
subject
=
None
,
smtphost
=
None
,
port
=
'25'
,
encode
=
None
)
smtphost
=
None
has_key
=
args
.
has_key
if
has_key
(
'mailhost'
):
mailhost
=
args
[
'mailhost'
]
elif
has_key
(
'smtphost'
):
mailhost
=
smtphost
=
args
[
'smtphost'
]
elif
has_key
(
''
):
mailhost
=
args
[
'mailhost'
]
=
args
[
''
]
else
:
raise
MailHostError
,
'No mailhost was specified in tag'
for
key
in
(
'mailto'
,
'mailfrom'
,
'subject'
,
'port'
):
if
not
args
.
has_key
(
key
):
args
[
key
]
=
''
if
has_key
(
'encode'
)
and
args
[
'encode'
]
not
in
\
(
'base64'
,
'quoted-printable'
,
'uuencode'
,
'x-uuencode'
,
'uue'
,
'x-uue'
):
raise
MailHostError
,
(
'An unsupported encoding was specified in tag'
)
if
not
smtphost
:
self
.
__name__
=
self
.
mailhost
=
mailhost
self
.
smtphost
=
None
else
:
self
.
__name__
=
self
.
smtphost
=
smtphost
self
.
mailhost
=
None
self
.
section
=
section
self
.
args
=
args
self
.
mailto
=
args
[
'mailto'
]
self
.
mailfrom
=
args
[
'mailfrom'
]
self
.
subject
=
None
or
args
[
'subject'
]
if
args
[
'port'
]
and
type
(
args
[
'port'
])
is
type
(
's'
):
self
.
port
=
args
[
'port'
]
=
int
(
args
[
'port'
])
elif
args
[
'port'
]
==
''
:
self
.
port
=
args
[
'port'
]
=
25
else
:
self
.
port
=
args
[
'port'
]
if
has_key
(
'encode'
):
self
.
encode
=
args
[
'encode'
]
else
:
self
.
encode
=
None
def
render
(
self
,
md
):
args
=
self
.
args
has_key
=
args
.
has_key
if
self
.
mailhost
:
mhost
=
md
[
self
.
mailhost
]
elif
self
.
smtphost
:
mhost
=
MailBase
(
smtp_host
=
self
.
smtphost
,
smtp_port
=
self
.
port
)
mhost
.
send
(
render_blocks
(
self
.
section
.
blocks
,
md
),
self
.
mailto
,
self
.
mailfrom
,
self
.
subject
,
self
.
encode
)
return
' '
__call__
=
render
String
.
commands
[
'sendmail'
]
=
SendMailTag
src/Products/MailHost/__init__.py
deleted
100644 → 0
View file @
4c48339e
##############################################################################
#
# Copyright (c) 2002 Zope Foundation and Contributors.
#
# This software is subject to the provisions of the Zope Public License,
# Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution.
# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
# FOR A PARTICULAR PURPOSE
#
##############################################################################
__doc__
=
'''MailHost Product Initialization
$Id$'''
__version__
=
'$Revision: 1.22 $'
[
11
:
-
2
]
import
MailHost
import
SendMailTag
def
initialize
(
context
):
context
.
registerClass
(
MailHost
.
MailHost
,
permission
=
'Add MailHost objects'
,
constructors
=
(
MailHost
.
manage_addMailHostForm
,
MailHost
.
manage_addMailHost
),
icon
=
'www/MailHost_icon.gif'
,
)
context
.
registerHelp
()
context
.
registerHelpTitle
(
'Zope Help'
)
src/Products/MailHost/decorator.py
deleted
100644 → 0
View file @
4c48339e
##############################################################################
#
# Copyright (c) 2002 Zope Foundation and Contributors.
#
# This software is subject to the provisions of the Zope Public License,
# Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution.
# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
# FOR A PARTICULAR PURPOSE.
#
##############################################################################
"""
Decorator(s)
$Id: MailHost.py 78992 2007-08-19 11:58:08Z andreasjung $
"""
def
synchronized
(
lock
):
""" Decorator for method synchronization. """
def
wrapper
(
f
):
def
method
(
*
args
,
**
kw
):
lock
.
acquire
()
try
:
return
f
(
*
args
,
**
kw
)
finally
:
lock
.
release
()
return
method
return
wrapper
src/Products/MailHost/dtml/addMailHost_form.dtml
deleted
100644 → 0
View file @
4c48339e
<dtml-var manage_page_header>
<dtml-var "manage_form_title(this(), _,
form_title='Add MailHost',
help_product='MailHost',
help_topic='Mail-Host_Add.stx'
)">
<p class="form-help">
MailHost object provide a way to send email from Zope code in DTML or
Python Scripts. <em>SMTP host</em> is the name of the mail server machine.
<em>SMTP port</em> is the port on which the mail server is running the
SMTP service.
</p>
<form action="manage_addMailHost" method="post">
<table cellspacing="0" cellpadding="2" border="0">
<tr>
<td align="left" valign="top">
<div class="form-label">
Id
</div>
</td>
<td align="left" valign="top">
<input type="text" name="id" size="40" value="MailHost" />
</td>
</tr>
<tr>
<td align="left" valign="top">
<div class="form-optional">
Title
</div>
</td>
<td align="left" valign="top">
<input type="text" name="title" size="40" />
</td>
</tr>
<tr>
<td align="left" valign="top">
<div class="form-label">
SMTP Host
</div>
</td>
<td align="left" valign="top">
<input type="text" name="smtp_host" size="40" value="localhost" />
</td>
</tr>
<tr>
<td align="left" valign="top">
<div class="form-label">
SMTP Port
</div>
</td>
<td align="left" valign="top">
<input type="text" name="smtp_port:int" size="4" value="25" />
</td>
</tr>
<tr>
<td align="left" valign="top">
</td>
<td align="left" valign="top">
<div class="form-element">
<input class="form-element" type="submit" name="submit"
value=" Add " />
</div>
</td>
</tr>
</table>
</form>
<dtml-var manage_page_footer>
src/Products/MailHost/dtml/manageMailHost.dtml
deleted
100644 → 0
View file @
4c48339e
<dtml-var manage_page_header>
<dtml-var manage_tabs>
<form action="manage_makeChanges" method="post">
<table cellspacing="0" cellpadding="2" border="0">
<tr>
<td align="left" valign="top">
<div class="form-label">
Id
</div>
</td>
<td align="left" valign="top">
<div class="form-text">
&dtml-id;
</div>
</td>
</tr>
<tr>
<td align="left" valign="top">
<div class="form-optional">
Title
</div>
</td>
<td align="left" valign="top">
<input type="text" name="title" size="40"
value="&dtml-title;"/>
</td>
</tr>
<tr>
<td align="left" valign="top">
<div class="form-label">
SMTP Host
</div>
</td>
<td align="left" valign="top">
<input type="text" name="smtp_host" size="40"
value="&dtml-smtp_host;"/>
</td>
</tr>
<tr>
<td align="left" valign="top">
<div class="form-label">
SMTP Port
</div>
</td>
<td align="left" valign="top">
<input type="text" name="smtp_port:int" size="4"
value="&dtml-smtp_port;"/>
</td>
</tr>
<tr>
<td align="left" valign="top">
<div class="form-label">
Username
</div>
</td>
<td align="left" valign="top">
<input type="text" name="smtp_uid" size="15"
value="&dtml.null-smtp_uid;"/>
</td>
<td>
<span class="form-help">(optional for SMTP AUTH)</span>
</td>
</tr>
<tr>
<td align="left" valign="top">
<div class="form-label">
Password
</div>
</td>
<td align="left" valign="top">
<input type="password" name="smtp_pwd" size="15"
value="&dtml.null-smtp_pwd;"/>
</td>
<td>
<span class="form-help">(optional for SMTP AUTH)</span>
</td>
</tr>
<tr>
<td align="left" valign="top">
<div class="form-label">
Force TLS
</div>
</td>
<td align="left" valign="top">
<input type="checkbox" name="force_tls:boolean" value="1"
<dtml-if "force_tls">checked</dtml-if>
</td>
<td>
<span class="form-help">(enforce the use of an encrypted connection
to the SMTP server. Mail delivery fails if the SMTP server
does not support encryption)
</span>
</td>
</tr>
<tr>
<td align="left" valign="top">
<div class="form-label">
Use mail queue
</div>
</td>
<td align="left" valign="top">
<input type="checkbox" name="smtp_queue:boolean" value="1"
<dtml-if "smtp_queue">checked</dtml-if>
</td>
<td>
<span class="form-help">(asynchronous mail delivery if checked)</span>
</td>
</tr>
<tr>
<td align="left" valign="top">
<div class="form-label">
Queue directory<br/>
</div>
</td>
<td align="left" valign="top">
<input type="text" name="smtp_queue_directory" size="30"
value="&dtml-smtp_queue_directory;"/>
</td>
<td>
<span class="form-help">(directory on the filesystem where the mails will be spooled. Only used if 'Use mail queue' is checked.)</span>
</td>
</tr>
<tr>
<td align="left" valign="top">
</td>
<td align="left" valign="top">
<div class="form-element">
<input class="form-element" type="submit" name="submit"
value="Save Changes" />
</div>
</td>
</tr>
</table>
<dtml-if smtp_queue>
<br />
<table cellspacing="0" cellpadding="2" border="0">
<tr>
<td align="left" valign="top">
<div class="form-label">
Mails in queue <br/>
</div>
</td>
<td align="left" valign="top">
<span class="form-help"><dtml-var queueLength></span>
</td>
</tr>
<tr>
<td align="left" valign="top">
<div class="form-label">
Status of queue processor thread<br/>
</div>
</td>
<td align="left" valign="top">
<div class="form-help">
<dtml-if "queueThreadAlive()">
Running
<br/>
<a href="manage_restartQueueThread?action=stop">Stop queue processor thread</a> (this may take some seconds)
</dtml-if>
<dtml-if "not queueThreadAlive()">
Stopped
<br/>
<a href="manage_restartQueueThread?action=start">Start queue processor thread</a> (this may take some seconds)
</dtml-if>
</div>
</td>
</tr>
</table>
</dtml-if>
</form>
<dtml-var manage_page_footer>
src/Products/MailHost/help/Mail-Host.stx
deleted
100644 → 0
View file @
4c48339e
MailHost: Sends mail through an SMTP server.
MailHosts allow you to send mail via the Simple Mail Transfer
Protocol (SMTP).
This object can be used deliver mail by the <dtml-sendmail> tag
or via the send() and simple_send() methods.
'send(messageText, mto=None, mfrom=None, subject=None, encode=None)'
Sends an email message where the messageText is an rfc822 formatted
message. This allows you complete control over the message headers,
including setting any extra headers such as Cc: and Reply-To:.
The arguments are:
messageText -- The mail message. It can either be a rfc822
formed text with header fields, or just a body without any
header fields. The other arguments given will override the
header fields in the message, if they exist.
mto -- A commaseparated string or list of recipient(s) of the message.
mfrom -- The address of the message sender.
subject -- The subject of the message.
encode -- The rfc822 defined encoding of the message. The
default of 'None' means no encoding is done. Valid values
are 'base64', 'quoted-printable' and 'uuencode'.
'simple_send(self, mto, mfrom, subject, body)'
Sends a message. Only To:, From: and Subject: headers can be set.
Note that simple_send does not process or validate its arguments
in any way.
The arguments are:
mto -- A commaseparated string of recipient(s) of the message.
mfrom -- The address of the message sender.
subject -- The subject of the message.
body -- The body of the message.
src/Products/MailHost/help/Mail-Host_Add.stx
deleted
100644 → 0
View file @
4c48339e
MailHost - Add: Create a new MailHost
Description
Create a new MailHost object.
Controls
'ID' -- The id of the MailHost object.
'Title' -- The title of the MailHost.
'SMTP host' -- The domain name or address of the SMTP mail server
to relay mail through.
'SMTP port' -- The port of the SMTP mail server to relay mail
through.
src/Products/MailHost/help/Mail-Host_Edit.stx
deleted
100644 → 0
View file @
4c48339e
MailHost - Edit: Edit mail host properties
Description
This view allows you edit the MailHost.
Controls
'ID' -- The id of the MailHost.
'Title' -- The title of the MailHost.
'SMTP host' -- The domain name or address of the SMTP mail server
to relay mail through.
'SMTP port' -- The port of the SMTP mail server to relay mail
through.
src/Products/MailHost/help/MailHost.py
deleted
100644 → 0
View file @
4c48339e
##############################################################################
#
# Copyright (c) 2002 Zope Foundation and Contributors.
#
# This software is subject to the provisions of the Zope Public License,
# Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution.
# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
# FOR A PARTICULAR PURPOSE
#
##############################################################################
def
manage_addMailHost
(
id
,
title
=
''
,
smtp_host
=
None
,
localhost
=
'localhost'
,
smtp_port
=
25
,
timeout
=
1.0
):
"""
Add a mailhost object to an ObjectManager.
"""
class
MailHost
:
"""
MailHost objects work as adapters to Simple Mail Transfer Protocol
(SMTP) servers. MailHosts are used by DTML 'sendmail' tags
to find the proper host to deliver mail to.
"""
def
send
(
messageText
,
mto
=
None
,
mfrom
=
None
,
subject
=
None
,
encode
=
None
):
"""
Sends an email message where the messageText is an rfc822 formatted
message. This allows you complete control over the message headers,
including setting any extra headers such as Cc: and Reply-To:.
The arguments are:
messageText -- The mail message. It can either be a rfc822
formed text with header fields, or just a body without any
header fields. The other arguments given will override the
header fields in the message, if they exist.
mto -- A commaseparated string or list of recipient(s) of the message.
mfrom -- The address of the message sender.
subject -- The subject of the message.
encode -- The rfc822 defined encoding of the message. The
default of 'None' means no encoding is done. Valid values
are 'base64', 'quoted-printable' and 'uuencode'.
"""
def
simple_send
(
self
,
mto
,
mfrom
,
subject
,
body
):
"""
Sends a message. Only To:, From: and Subject: headers can be set.
Note that simple_send does not process or validate its arguments
in any way.
The arguments are:
mto -- A commaseparated string of recipient(s) of the message.
mfrom -- The address of the message sender.
subject -- The subject of the message.
body -- The body of the message.
"""
src/Products/MailHost/interfaces.py
deleted
100644 → 0
View file @
4c48339e
##############################################################################
#
# Copyright (c) 2005 Zope Foundation and Contributors.
#
# This software is subject to the provisions of the Zope Public License,
# Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution.
# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
# FOR A PARTICULAR PURPOSE.
#
##############################################################################
"""MailHost z3 interfaces.
$Id$
"""
from
zope.interface
import
Interface
class
IMailHost
(
Interface
):
def
send
(
messageText
,
mto
=
None
,
mfrom
=
None
,
subject
=
None
,
encode
=
None
,
charset
=
None
,
msg_type
=
None
):
"""Send mail.
"""
src/Products/MailHost/mailer.py
deleted
100644 → 0
View file @
4c48339e
import
zope.deferredimport
zope
.
deferredimport
.
deprecatedFrom
(
"Import from zope.sendmail instead"
,
'zope.sendmail.mailer'
,
'SMTPMailer'
,
)
src/Products/MailHost/tests/__init__.py
deleted
100644 → 0
View file @
4c48339e
##############################################################################
#
# Copyright (c) 2003 Zope Foundation and Contributors.
# All Rights Reserved.
#
# This software is subject to the provisions of the Zope Public License,
# Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution.
# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
# FOR A PARTICULAR PURPOSE.
#
##############################################################################
# This file is needed to make this a package.
src/Products/MailHost/tests/testMailHost.py
deleted
100644 → 0
View file @
4c48339e
##############################################################################
#
# Copyright (c) 2002 Zope Foundation and Contributors.
#
# This software is subject to the provisions of the Zope Public License,
# Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution.
# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
# FOR A PARTICULAR PURPOSE.
#
##############################################################################
"""MailHost unit tests.
$Id$
"""
import
unittest
from
email
import
message_from_string
from
Products.MailHost.MailHost
import
MailHost
from
Products.MailHost.MailHost
import
MailHostError
,
_mungeHeaders
class
DummyMailHost
(
MailHost
):
meta_type
=
'Dummy Mail Host'
def
__init__
(
self
,
id
):
self
.
id
=
id
self
.
sent
=
''
def
_send
(
self
,
mfrom
,
mto
,
messageText
,
immediate
=
False
):
self
.
sent
=
messageText
self
.
immediate
=
immediate
class
FakeContent
(
object
):
def
__init__
(
self
,
template_name
,
message
):
def
template
(
self
,
context
,
REQUEST
=
None
):
return
message
setattr
(
self
,
template_name
,
template
)
@
staticmethod
def
check_status
(
context
,
REQUEST
=
None
):
return
'Message Sent'
class
TestMailHost
(
unittest
.
TestCase
):
def
_getTargetClass
(
self
):
return
DummyMailHost
def
_makeOne
(
self
,
*
args
,
**
kw
):
return
self
.
_getTargetClass
()(
*
args
,
**
kw
)
def
test_z3interfaces
(
self
):
from
Products.MailHost.interfaces
import
IMailHost
from
zope.interface.verify
import
verifyClass
verifyClass
(
IMailHost
,
self
.
_getTargetClass
())
def
testAllHeaders
(
self
):
msg
=
"""To: recipient@domain.com
From: sender@domain.com
Subject: This is the subject
This is the message body."""
# No additional info
resmsg
,
resto
,
resfrom
=
_mungeHeaders
(
msg
)
self
.
failUnless
(
resto
==
[
'recipient@domain.com'
])
self
.
failUnless
(
resfrom
==
'sender@domain.com'
)
# Add duplicated info
resmsg
,
resto
,
resfrom
=
_mungeHeaders
(
msg
,
'recipient@domain.com'
,
'sender@domain.com'
,
'This is the subject'
)
self
.
failUnlessEqual
(
resto
,
[
'recipient@domain.com'
])
self
.
failUnlessEqual
(
resfrom
,
'sender@domain.com'
)
# Add extra info
resmsg
,
resto
,
resfrom
=
_mungeHeaders
(
msg
,
'recipient2@domain.com'
,
'sender2@domain.com'
,
'This is the real subject'
)
self
.
failUnlessEqual
(
resto
,
[
'recipient2@domain.com'
])
self
.
failUnlessEqual
(
resfrom
,
'sender2@domain.com'
)
def
testMissingHeaders
(
self
):
msg
=
"""X-Header: Dummy header
This is the message body."""
# Doesn't specify to
self
.
failUnlessRaises
(
MailHostError
,
_mungeHeaders
,
msg
,
mfrom
=
'sender@domain.com'
)
# Doesn't specify from
self
.
failUnlessRaises
(
MailHostError
,
_mungeHeaders
,
msg
,
mto
=
'recipient@domain.com'
)
def
testNoHeaders
(
self
):
msg
=
"""This is the message body."""
# Doesn't specify to
self
.
failUnlessRaises
(
MailHostError
,
_mungeHeaders
,
msg
,
mfrom
=
'sender@domain.com'
)
# Doesn't specify from
self
.
failUnlessRaises
(
MailHostError
,
_mungeHeaders
,
msg
,
mto
=
'recipient@domain.com'
)
# Specify all
resmsg
,
resto
,
resfrom
=
_mungeHeaders
(
msg
,
'recipient2@domain.com'
,
'sender2@domain.com'
,
'This is the real subject'
)
self
.
failUnlessEqual
(
resto
,
[
'recipient2@domain.com'
])
self
.
failUnlessEqual
(
resfrom
,
'sender2@domain.com'
)
def
testBCCHeader
(
self
):
msg
=
"From: me@example.com
\
n
Bcc: many@example.com
\
n
\
n
Message text"
# Specify only the "Bcc" header. Useful for bulk emails.
resmsg
,
resto
,
resfrom
=
_mungeHeaders
(
msg
)
self
.
failUnlessEqual
(
resto
,
[
'many@example.com'
])
self
.
failUnlessEqual
(
resfrom
,
'me@example.com'
)
def
test__getThreadKey_uses_fspath
(
self
):
mh1
=
self
.
_makeOne
(
'mh1'
)
mh1
.
smtp_queue_directory
=
'/abc'
mh1
.
absolute_url
=
lambda
self
:
'http://example.com/mh1'
mh2
=
self
.
_makeOne
(
'mh2'
)
mh2
.
smtp_queue_directory
=
'/abc'
mh2
.
absolute_url
=
lambda
self
:
'http://example.com/mh2'
self
.
assertEqual
(
mh1
.
_getThreadKey
(),
mh2
.
_getThreadKey
())
def
testAddressParser
(
self
):
msg
=
"""To: "Name, Nick" <recipient@domain.com>, "Foo Bar" <foo@domain.com>
CC: "Web, Jack" <jack@web.com>
From: sender@domain.com
Subject: This is the subject
This is the message body."""
# Test Address-Parser for To & CC given in messageText
resmsg
,
resto
,
resfrom
=
_mungeHeaders
(
msg
)
self
.
failUnlessEqual
(
resto
,
[
'"Name, Nick" <recipient@domain.com>'
,
'Foo Bar <foo@domain.com>'
,
'"Web, Jack" <jack@web.com>'
])
self
.
failUnlessEqual
(
resfrom
,
'sender@domain.com'
)
# Test Address-Parser for a given mto-string
resmsg
,
resto
,
resfrom
=
_mungeHeaders
(
msg
,
mto
=
'"Public, Joe" <pjoe@domain.com>, Foo Bar <foo@domain.com>'
)
self
.
failUnlessEqual
(
resto
,
[
'"Public, Joe" <pjoe@domain.com>'
,
'Foo Bar <foo@domain.com>'
])
self
.
failUnlessEqual
(
resfrom
,
'sender@domain.com'
)
def
testSendMessageOnly
(
self
):
msg
=
"""
\
To: "Name, Nick" <recipient@domain.com>, "Foo Bar" <foo@domain.com>
From: sender@domain.com
Subject: This is the subject
Date: Sun, 27 Aug 2006 17:00:00 +0200
This is the message body."""
mailhost
=
self
.
_makeOne
(
'MailHost'
)
mailhost
.
send
(
msg
)
self
.
assertEqual
(
mailhost
.
sent
,
msg
)
def
testSendWithArguments
(
self
):
inmsg
=
"""
\
Date: Sun, 27 Aug 2006 17:00:00 +0200
This is the message body."""
outmsg
=
"""
\
Date: Sun, 27 Aug 2006 17:00:00 +0200
Subject: This is the subject
To: "Name, Nick" <recipient@domain.com>, Foo Bar <foo@domain.com>
From: sender@domain.com
This is the message body."""
mailhost
=
self
.
_makeOne
(
'MailHost'
)
mailhost
.
send
(
messageText
=
inmsg
,
mto
=
'"Name, Nick" <recipient@domain.com>, "Foo Bar" <foo@domain.com>'
,
mfrom
=
'sender@domain.com'
,
subject
=
'This is the subject'
)
self
.
assertEqual
(
mailhost
.
sent
,
outmsg
)
def
testSendWithMtoList
(
self
):
inmsg
=
"""
\
Date: Sun, 27 Aug 2006 17:00:00 +0200
This is the message body."""
outmsg
=
"""
\
Date: Sun, 27 Aug 2006 17:00:00 +0200
Subject: This is the subject
To: "Name, Nick" <recipient@domain.com>, Foo Bar <foo@domain.com>
From: sender@domain.com
This is the message body."""
mailhost
=
self
.
_makeOne
(
'MailHost'
)
mailhost
.
send
(
messageText
=
inmsg
,
mto
=
[
'"Name, Nick" <recipient@domain.com>'
,
'"Foo Bar" <foo@domain.com>'
],
mfrom
=
'sender@domain.com'
,
subject
=
'This is the subject'
)
self
.
assertEqual
(
mailhost
.
sent
,
outmsg
)
def
testSimpleSend
(
self
):
outmsg
=
"""
\
From: sender@domain.com
To: "Name, Nick" <recipient@domain.com>, "Foo Bar" <foo@domain.com>
Subject: This is the subject
This is the message body."""
mailhost
=
self
.
_makeOne
(
'MailHost'
)
mailhost
.
simple_send
(
mto
=
'"Name, Nick" <recipient@domain.com>, "Foo Bar" <foo@domain.com>'
,
mfrom
=
'sender@domain.com'
,
subject
=
'This is the subject'
,
body
=
'This is the message body.'
)
self
.
assertEqual
(
mailhost
.
sent
,
outmsg
)
self
.
assertEqual
(
mailhost
.
immediate
,
False
)
def
testSendImmediate
(
self
):
outmsg
=
"""
\
From: sender@domain.com
To: "Name, Nick" <recipient@domain.com>, "Foo Bar" <foo@domain.com>
Subject: This is the subject
This is the message body."""
mailhost
=
self
.
_makeOne
(
'MailHost'
)
mailhost
.
simple_send
(
mto
=
'"Name, Nick" <recipient@domain.com>, "Foo Bar" <foo@domain.com>'
,
mfrom
=
'sender@domain.com'
,
subject
=
'This is the subject'
,
body
=
'This is the message body.'
,
immediate
=
True
)
self
.
assertEqual
(
mailhost
.
sent
,
outmsg
)
self
.
assertEqual
(
mailhost
.
immediate
,
True
)
def
testSendBodyWithUrl
(
self
):
# The implementation of rfc822.Message reacts poorly to
# message bodies containing ':' characters as in a url
msg
=
"Here's a nice link: http://www.zope.org/"
mailhost
=
self
.
_makeOne
(
'MailHost'
)
mailhost
.
send
(
messageText
=
msg
,
mto
=
'"Name, Nick" <recipient@domain.com>, "Foo Bar" <foo@domain.com>'
,
mfrom
=
'sender@domain.com'
,
subject
=
'This is the subject'
)
out
=
message_from_string
(
mailhost
.
sent
)
self
.
failUnlessEqual
(
out
.
get_payload
(),
msg
)
self
.
failUnlessEqual
(
out
[
'To'
],
'"Name, Nick" <recipient@domain.com>, Foo Bar <foo@domain.com>'
)
self
.
failUnlessEqual
(
out
[
'From'
],
'sender@domain.com'
)
def
testSendEncodedBody
(
self
):
# If a charset is specified the correct headers for content
# encoding will be set if not already set. Additionally, if
# there is a default transfer encoding for the charset, then
# the content will be encoded and the transfer encoding header
# will be set.
msg
=
"Here's some encoded t
\
xc3
\
xa9
xt."
mailhost
=
self
.
_makeOne
(
'MailHost'
)
mailhost
.
send
(
messageText
=
msg
,
mto
=
'"Name, Nick" <recipient@domain.com>, "Foo Bar" <foo@domain.com>'
,
mfrom
=
'sender@domain.com'
,
subject
=
'This is the subject'
,
charset
=
'utf-8'
)
out
=
message_from_string
(
mailhost
.
sent
)
self
.
failUnlessEqual
(
out
[
'To'
],
'"Name, Nick" <recipient@domain.com>, Foo Bar <foo@domain.com>'
)
self
.
failUnlessEqual
(
out
[
'From'
],
'sender@domain.com'
)
# utf-8 will default to Quoted Printable encoding
self
.
failUnlessEqual
(
out
[
'Content-Transfer-Encoding'
],
'quoted-printable'
)
self
.
failUnlessEqual
(
out
[
'Content-Type'
],
'text/plain; charset="utf-8"'
)
self
.
failUnlessEqual
(
out
.
get_payload
(),
"Here's some encoded t=C3=A9xt."
)
def
testEncodedHeaders
(
self
):
# Headers are encoded automatically, email headers are encoded
# piece-wise to ensure the adresses remain ASCII
mfrom
=
"Jos
\
xc3
\
xa9
Andr
\
xc3
\
xa9
s <jose@example.com>"
mto
=
"Ferran Adri
\
xc3
\
xa0
<ferran@example.com>"
subject
=
"
\
xc2
\
xbf
Esferificaci
\
xc3
\
xb3
n?"
mailhost
=
self
.
_makeOne
(
'MailHost'
)
mailhost
.
send
(
messageText
=
'A message.'
,
mto
=
mto
,
mfrom
=
mfrom
,
subject
=
subject
,
charset
=
'utf-8'
)
out
=
message_from_string
(
mailhost
.
sent
)
self
.
failUnlessEqual
(
out
[
'To'
],
'=?utf-8?q?Ferran_Adri=C3=A0?= <ferran@example.com>'
)
self
.
failUnlessEqual
(
out
[
'From'
],
'=?utf-8?q?Jos=C3=A9_Andr=C3=A9s?= <jose@example.com>'
)
self
.
failUnlessEqual
(
out
[
'Subject'
],
'=?utf-8?q?=C2=BFEsferificaci=C3=B3n=3F?='
)
# utf-8 will default to Quoted Printable encoding
self
.
failUnlessEqual
(
out
[
'Content-Transfer-Encoding'
],
'quoted-printable'
)
self
.
failUnlessEqual
(
out
[
'Content-Type'
],
'text/plain; charset="utf-8"'
)
self
.
failUnlessEqual
(
out
.
get_payload
(),
"A message."
)
def
testAlreadyEncodedMessage
(
self
):
# If the message already specifies encodings, it is
# essentially not altered this is true even if charset or
# msg_type is specified
msg
=
"""
\
From: =?utf-8?q?Jos=C3=A9_Andr=C3=A9s?= <jose@example.com>
To: =?utf-8?q?Ferran_Adri=C3=A0?= <ferran@example.com>
Subject: =?utf-8?q?=C2=BFEsferificaci=C3=B3n=3F?=
Date: Sun, 27 Aug 2006 17:00:00 +0200
Content-Type: text/html; charset="utf-8"
Content-Transfer-Encoding: base64
MIME-Version: 1.0 (Generated by testMailHost.py)
wqFVbiB0cnVjbyA8c3Ryb25nPmZhbnTDoXN0aWNvPC9zdHJvbmc+IQ=3D=3D
"""
mailhost
=
self
.
_makeOne
(
'MailHost'
)
mailhost
.
send
(
messageText
=
msg
)
self
.
failUnlessEqual
(
mailhost
.
sent
,
msg
)
mailhost
.
send
(
messageText
=
msg
,
msg_type
=
'text/plain'
)
# The msg_type is ignored if already set
self
.
failUnlessEqual
(
mailhost
.
sent
,
msg
)
def
testAlreadyEncodedMessageWithCharset
(
self
):
# If the message already specifies encodings, it is
# essentially not altered this is true even if charset or
# msg_type is specified
msg
=
"""
\
From: =?utf-8?q?Jos=C3=A9_Andr=C3=A9s?= <jose@example.com>
To: =?utf-8?q?Ferran_Adri=C3=A0?= <ferran@example.com>
Subject: =?utf-8?q?=C2=BFEsferificaci=C3=B3n=3F?=
Date: Sun, 27 Aug 2006 17:00:00 +0200
Content-Type: text/html; charset="utf-8"
Content-Transfer-Encoding: base64
MIME-Version: 1.0 (Generated by testMailHost.py)
wqFVbiB0cnVjbyA8c3Ryb25nPmZhbnTDoXN0aWNvPC9zdHJvbmc+IQ=3D=3D
"""
mailhost
=
self
.
_makeOne
(
'MailHost'
)
# Pass a different charset, which will apply to any explicitly
# set headers
mailhost
.
send
(
messageText
=
msg
,
subject
=
'
\
xbf
Esferificaci
\
xf3
n?'
,
charset
=
'iso-8859-1'
,
msg_type
=
'text/plain'
)
# The charset for the body should remain the same, but any
# headers passed into the method will be encoded using the
# specified charset
out
=
message_from_string
(
mailhost
.
sent
)
self
.
failUnlessEqual
(
out
[
'Content-Type'
],
'text/html; charset="utf-8"'
)
self
.
failUnlessEqual
(
out
[
'Content-Transfer-Encoding'
],
'base64'
)
# Headers set by parameter will be set using charset parameter
self
.
failUnlessEqual
(
out
[
'Subject'
],
'=?iso-8859-1?q?=BFEsferificaci=F3n=3F?='
)
# original headers will be unaltered
self
.
failUnlessEqual
(
out
[
'From'
],
'=?utf-8?q?Jos=C3=A9_Andr=C3=A9s?= <jose@example.com>'
)
def
testUnicodeMessage
(
self
):
# unicode messages and headers are decoded using the given charset
msg
=
unicode
(
"Here's some unencoded <strong>t
\
xc3
\
xa9
xt</strong>."
,
'utf-8'
)
mfrom
=
unicode
(
'Ferran Adri
\
xc3
\
xa0
<ferran@example.com>'
,
'utf-8'
)
subject
=
unicode
(
'
\
xc2
\
xa1
Andr
\
xc3
\
xa9
s!'
,
'utf-8'
)
mailhost
=
self
.
_makeOne
(
'MailHost'
)
mailhost
.
send
(
messageText
=
msg
,
mto
=
'"Name, Nick" <recipient@domain.com>'
,
mfrom
=
mfrom
,
subject
=
subject
,
charset
=
'utf-8'
,
msg_type
=
'text/html'
)
out
=
message_from_string
(
mailhost
.
sent
)
self
.
failUnlessEqual
(
out
[
'To'
],
'"Name, Nick" <recipient@domain.com>'
)
self
.
failUnlessEqual
(
out
[
'From'
],
'=?utf-8?q?Ferran_Adri=C3=A0?= <ferran@example.com>'
)
self
.
failUnlessEqual
(
out
[
'Subject'
],
'=?utf-8?q?=C2=A1Andr=C3=A9s!?='
)
self
.
failUnlessEqual
(
out
[
'Content-Transfer-Encoding'
],
'quoted-printable'
)
self
.
failUnlessEqual
(
out
[
'Content-Type'
],
'text/html; charset="utf-8"'
)
self
.
failUnlessEqual
(
out
.
get_payload
(),
"Here's some unencoded <strong>t=C3=A9xt</strong>."
)
def
testUnicodeNoEncodingErrors
(
self
):
# Unicode messages and headers raise errors if no charset is passed to
# send
msg
=
unicode
(
"Here's some unencoded <strong>t
\
xc3
\
xa9
xt</strong>."
,
'utf-8'
)
subject
=
unicode
(
'
\
xc2
\
xa1
Andr
\
xc3
\
xa9
s!'
,
'utf-8'
)
mailhost
=
self
.
_makeOne
(
'MailHost'
)
self
.
assertRaises
(
UnicodeEncodeError
,
mailhost
.
send
,
msg
,
mto
=
'"Name, Nick" <recipient@domain.com>'
,
mfrom
=
'Foo Bar <foo@domain.com>'
,
subject
=
subject
)
def
testUnicodeDefaultEncoding
(
self
):
# However if we pass unicode that can be encoded to the
# default encoding (generally 'us-ascii'), no error is raised.
# We include a date in the messageText to make inspecting the
# results more convenient.
msg
=
u"""
\
Date: Sun, 27 Aug 2006 17:00:00 +0200
Here's some unencoded <strong>text</strong>."""
subject
=
u'Andres!'
mailhost
=
self
.
_makeOne
(
'MailHost'
)
mailhost
.
send
(
msg
,
mto
=
u'"Name, Nick" <recipient@domain.com>'
,
mfrom
=
u'Foo Bar <foo@domain.com>'
,
subject
=
subject
)
out
=
mailhost
.
sent
# Ensure the results are not unicode
self
.
failUnlessEqual
(
out
,
"""
\
Date: Sun, 27 Aug 2006 17:00:00 +0200
Subject: Andres!
To: "Name, Nick" <recipient@domain.com>
From: Foo Bar <foo@domain.com>
Here's some unencoded <strong>text</strong>."""
)
self
.
failUnlessEqual
(
type
(
out
),
str
)
def
testSendMessageObject
(
self
):
# send will accept an email.Message.Message object directly
msg
=
message_from_string
(
"""
\
From: =?utf-8?q?Jos=C3=A9_Andr=C3=A9s?= <jose@example.com>
To: =?utf-8?q?Ferran_Adri=C3=A0?= <ferran@example.com>
Subject: =?utf-8?q?=C2=BFEsferificaci=C3=B3n=3F?=
Date: Sun, 27 Aug 2006 17:00:00 +0200
Content-Type: text/html; charset="utf-8"
Content-Transfer-Encoding: base64
MIME-Version: 1.1
wqFVbiB0cnVjbyA8c3Ryb25nPmZhbnTDoXN0aWNvPC9zdHJvbmc+IQ=3D=3D
"""
)
mailhost
=
self
.
_makeOne
(
'MailHost'
)
mailhost
.
send
(
msg
)
out
=
message_from_string
(
mailhost
.
sent
)
self
.
failUnlessEqual
(
out
.
as_string
(),
msg
.
as_string
())
# we can even alter a from and subject headers without affecting the
# original object
mailhost
.
send
(
msg
,
mfrom
=
'Foo Bar <foo@domain.com>'
,
subject
=
'Changed!'
)
out
=
message_from_string
(
mailhost
.
sent
)
# We need to make sure we didn't mutate the message we were passed
self
.
failIfEqual
(
out
.
as_string
(),
msg
.
as_string
())
self
.
failUnlessEqual
(
out
[
'From'
],
'Foo Bar <foo@domain.com>'
)
self
.
failUnlessEqual
(
msg
[
'From'
],
'=?utf-8?q?Jos=C3=A9_Andr=C3=A9s?= <jose@example.com>'
)
# The subject is encoded with the body encoding since no
# explicit encoding was specified
self
.
failUnlessEqual
(
out
[
'Subject'
],
'=?utf-8?q?Changed!?='
)
self
.
failUnlessEqual
(
msg
[
'Subject'
],
'=?utf-8?q?=C2=BFEsferificaci=C3=B3n=3F?='
)
def
testExplicitUUEncoding
(
self
):
# We can request a payload encoding explicitly, though this
# should probably be considered deprecated functionality.
mailhost
=
self
.
_makeOne
(
'MailHost'
)
# uuencoding
mailhost
.
send
(
'Date: Sun, 27 Aug 2006 17:00:00 +0200
\
n
\
n
A Message'
,
mfrom
=
'sender@domain.com'
,
mto
=
'Foo Bar <foo@domain.com>'
,
encode
=
'uue'
)
out
=
message_from_string
(
mailhost
.
sent
)
self
.
failUnlessEqual
(
mailhost
.
sent
,
"""
\
Date: Sun, 27 Aug 2006 17:00:00 +0200
Subject: [No Subject]
To: Foo Bar <foo@domain.com>
From: sender@domain.com
Content-Transfer-Encoding: uue
Mime-Version: 1.0
begin 666 -
)02!-97-S86=E
end
"""
)
def
testExplicitBase64Encoding
(
self
):
mailhost
=
self
.
_makeOne
(
'MailHost'
)
mailhost
.
send
(
'Date: Sun, 27 Aug 2006 17:00:00 +0200
\
n
\
n
A Message'
,
mfrom
=
'sender@domain.com'
,
mto
=
'Foo Bar <foo@domain.com>'
,
encode
=
'base64'
)
out
=
message_from_string
(
mailhost
.
sent
)
self
.
failUnlessEqual
(
mailhost
.
sent
,
"""
\
Date: Sun, 27 Aug 2006 17:00:00 +0200
Subject: [No Subject]
To: Foo Bar <foo@domain.com>
From: sender@domain.com
Content-Transfer-Encoding: base64
Mime-Version: 1.0
QSBNZXNzYWdl"""
)
def
testExplicit7bitEncoding
(
self
):
mailhost
=
self
.
_makeOne
(
'MailHost'
)
mailhost
.
send
(
'Date: Sun, 27 Aug 2006 17:00:00 +0200
\
n
\
n
A Message'
,
mfrom
=
'sender@domain.com'
,
mto
=
'Foo Bar <foo@domain.com>'
,
encode
=
'7bit'
)
out
=
message_from_string
(
mailhost
.
sent
)
self
.
failUnlessEqual
(
mailhost
.
sent
,
"""
\
Date: Sun, 27 Aug 2006 17:00:00 +0200
Subject: [No Subject]
To: Foo Bar <foo@domain.com>
From: sender@domain.com
Content-Transfer-Encoding: 7bit
Mime-Version: 1.0
A Message"""
)
def
testExplicit8bitEncoding
(
self
):
mailhost
=
self
.
_makeOne
(
'MailHost'
)
# We pass an encoded string with unspecified charset, it should be
# encoded 8bit
mailhost
.
send
(
'Date: Sun, 27 Aug 2006 17:00:00 +0200
\
n
\
n
A M
\
xc3
\
xa9
ssage'
,
mfrom
=
'sender@domain.com'
,
mto
=
'Foo Bar <foo@domain.com>'
,
encode
=
'8bit'
)
out
=
message_from_string
(
mailhost
.
sent
)
self
.
failUnlessEqual
(
mailhost
.
sent
,
"""
\
Date: Sun, 27 Aug 2006 17:00:00 +0200
Subject: [No Subject]
To: Foo Bar <foo@domain.com>
From: sender@domain.com
Content-Transfer-Encoding: 8bit
Mime-Version: 1.0
A M
\
xc3
\
xa9
ssage"""
)
def
testSendTemplate
(
self
):
content
=
FakeContent
(
'my_template'
,
'A Message'
)
mailhost
=
self
.
_makeOne
(
'MailHost'
)
result
=
mailhost
.
sendTemplate
(
content
,
'my_template'
,
mto
=
'Foo Bar <foo@domain.com>'
,
mfrom
=
'sender@domain.com'
)
self
.
failUnlessEqual
(
result
,
'SEND OK'
)
result
=
mailhost
.
sendTemplate
(
content
,
'my_template'
,
mto
=
'Foo Bar <foo@domain.com>'
,
mfrom
=
'sender@domain.com'
,
statusTemplate
=
'wrong_name'
)
self
.
failUnlessEqual
(
result
,
'SEND OK'
)
result
=
mailhost
.
sendTemplate
(
content
,
'my_template'
,
mto
=
'Foo Bar <foo@domain.com>'
,
mfrom
=
'sender@domain.com'
,
statusTemplate
=
'check_status'
)
self
.
failUnlessEqual
(
result
,
'Message Sent'
)
def
testSendMultiPartAlternativeMessage
(
self
):
msg
=
(
"""
\
Content-Type: multipart/alternative; boundary="===============0490954888=="
MIME-Version: 1.0
Date: Sun, 27 Aug 2006 17:00:00 +0200
Subject: My multipart email
To: Foo Bar <foo@domain.com>
From: sender@domain.com
--===============0490954888==
Content-Type: text/plain; charset="utf-8"
MIME-Version: 1.0
Content-Transfer-Encoding: quoted-printable
This is plain text.
--===============0490954888==
Content-Type: text/html; charset="utf-8"
MIME-Version: 1.0
Content-Transfer-Encoding: quoted-printable
<p>This is html.</p>
--===============0490954888==--
"""
)
mailhost
=
self
.
_makeOne
(
'MailHost'
)
# Specifying a charset for the header may have unwanted side
# effects in the case of multipart mails.
# (TypeError: expected string or buffer)
mailhost
.
send
(
msg
,
charset
=
'utf-8'
)
self
.
assertEqual
(
mailhost
.
sent
,
msg
)
def
testSendMultiPartMixedMessage
(
self
):
msg
=
(
"""
\
Content-Type: multipart/mixed; boundary="XOIedfhf+7KOe/yw"
Content-Disposition: inline
MIME-Version: 1.0
Date: Sun, 27 Aug 2006 17:00:00 +0200
Subject: My multipart email
To: Foo Bar <foo@domain.com>
From: sender@domain.com
--XOIedfhf+7KOe/yw
Content-Type: text/plain; charset=us-ascii
Content-Disposition: inline
This is a test with as attachment OFS/www/new.gif.
--XOIedfhf+7KOe/yw
Content-Type: image/gif
Content-Disposition: attachment; filename="new.gif"
Content-Transfer-Encoding: base64
R0lGODlhCwAQAPcAAP8A/wAAAFBQUICAgMDAwP8AAIAAQAAAoABAgIAAgEAAQP//AP//gACA
gECAgP///wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACH5BAEAAAAALAAAAAALABAAAAg7AAEIFKhgoEGC
CwoeRKhwoYKEBhVIfLgg4UQAFCtqbJixYkOEHg9SHDmQJEmMEBkS/IiR5cKXMGPKDAgAOw==
--XOIedfhf+7KOe/yw
Content-Type: text/plain; charset=iso-8859-1
Content-Disposition: attachment; filename="test.txt"
Content-Transfer-Encoding: quoted-printable
D=EDt =EFs =E9=E9n test
--XOIedfhf+7KOe/yw--
"""
)
mailhost
=
self
.
_makeOne
(
'MailHost'
)
# Specifying a charset for the header may have unwanted side
# effects in the case of multipart mails.
# (TypeError: expected string or buffer)
mailhost
.
send
(
msg
,
charset
=
'utf-8'
)
self
.
assertEqual
(
mailhost
.
sent
,
msg
)
def
test_suite
():
suite
=
unittest
.
TestSuite
()
suite
.
addTest
(
unittest
.
makeSuite
(
TestMailHost
)
)
return
suite
if
__name__
==
'__main__'
:
unittest
.
main
(
defaultTest
=
'test_suite'
)
src/Products/MailHost/version.txt
deleted
100644 → 0
View file @
4c48339e
MailHost-1-4-0
src/Products/MailHost/www/MailHost_icon.gif
deleted
100644 → 0
View file @
4c48339e
896 Bytes
versions.cfg
View file @
c8a66859
...
@@ -17,6 +17,7 @@ nt-svcutils = 2.13.0
...
@@ -17,6 +17,7 @@ nt-svcutils = 2.13.0
Persistence = 2.13.2
Persistence = 2.13.2
Products.BTreeFolder2 = 2.13.0
Products.BTreeFolder2 = 2.13.0
Products.ExternalMethod = 2.13.0
Products.ExternalMethod = 2.13.0
Products.MailHost = 2.13.0
Products.MIMETools = 2.13.0
Products.MIMETools = 2.13.0
Products.OFSP = 2.13.1
Products.OFSP = 2.13.1
Products.PythonScripts = 2.13.0
Products.PythonScripts = 2.13.0
...
...
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