| 1 |
|
|---|
| 2 |
|
|---|
| 3 |
|
|---|
| 4 |
|
|---|
| 5 |
|
|---|
| 6 |
|
|---|
| 7 |
"""Simple Mail Transfer Protocol implementation. |
|---|
| 8 |
""" |
|---|
| 9 |
|
|---|
| 10 |
from __future__ import generators |
|---|
| 11 |
|
|---|
| 12 |
|
|---|
| 13 |
from twisted.protocols import basic |
|---|
| 14 |
from twisted.protocols import policies |
|---|
| 15 |
from twisted.internet import protocol |
|---|
| 16 |
from twisted.internet import defer |
|---|
| 17 |
from twisted.internet import error |
|---|
| 18 |
from twisted.internet import reactor |
|---|
| 19 |
from twisted.internet.interfaces import ITLSTransport |
|---|
| 20 |
from twisted.python import log |
|---|
| 21 |
from twisted.python import components |
|---|
| 22 |
from twisted.python import util |
|---|
| 23 |
from twisted.python import reflect |
|---|
| 24 |
from twisted.python import failure |
|---|
| 25 |
|
|---|
| 26 |
from twisted import cred |
|---|
| 27 |
import twisted.cred.checkers |
|---|
| 28 |
import twisted.cred.credentials |
|---|
| 29 |
|
|---|
| 30 |
|
|---|
| 31 |
import time, string, re, base64, types, socket, os, random, hmac |
|---|
| 32 |
import MimeWriter, tempfile, rfc822 |
|---|
| 33 |
import warnings |
|---|
| 34 |
import binascii |
|---|
| 35 |
import sys |
|---|
| 36 |
from zope.interface import implements |
|---|
| 37 |
|
|---|
| 38 |
try: |
|---|
| 39 |
from email.base64MIME import encode as encode_base64 |
|---|
| 40 |
except ImportError: |
|---|
| 41 |
def encode_base64(s, eol='\n'): |
|---|
| 42 |
return s.encode('base64').rstrip() + eol |
|---|
| 43 |
|
|---|
| 44 |
try: |
|---|
| 45 |
from cStringIO import StringIO |
|---|
| 46 |
except ImportError: |
|---|
| 47 |
from StringIO import StringIO |
|---|
| 48 |
|
|---|
| 49 |
DNSNAME = socket.getfqdn() |
|---|
| 50 |
|
|---|
| 51 |
|
|---|
| 52 |
SUCCESS = dict(map(None, range(200, 300), [])) |
|---|
| 53 |
|
|---|
| 54 |
class IMessageDelivery(components.Interface): |
|---|
| 55 |
def receivedHeader(self, helo, origin, recipients): |
|---|
| 56 |
""" |
|---|
| 57 |
Generate the Received header for a message |
|---|
| 58 |
|
|---|
| 59 |
@type helo: C{(str, str)} |
|---|
| 60 |
@param helo: The argument to the HELO command and the client's IP |
|---|
| 61 |
address. |
|---|
| 62 |
|
|---|
| 63 |
@type origin: C{Address} |
|---|
| 64 |
@param origin: The address the message is from |
|---|
| 65 |
|
|---|
| 66 |
@type recipients: C{list} of C{str} |
|---|
| 67 |
@param recipients: A list of the addresses for which this message |
|---|
| 68 |
is bound. |
|---|
| 69 |
|
|---|
| 70 |
@rtype: C{str} |
|---|
| 71 |
@return: The full "Received" header string. |
|---|
| 72 |
""" |
|---|
| 73 |
|
|---|
| 74 |
def validateTo(self, user): |
|---|
| 75 |
""" |
|---|
| 76 |
Validate the address for which the message is destined. |
|---|
| 77 |
|
|---|
| 78 |
@type user: C{User} |
|---|
| 79 |
@param user: The address to validate. |
|---|
| 80 |
|
|---|
| 81 |
@rtype: no-argument callable |
|---|
| 82 |
@return: A C{Deferred} which becomes, or a callable which |
|---|
| 83 |
takes no arguments and returns an object implementing C{IMessage}. |
|---|
| 84 |
This will be called and the returned object used to deliver the |
|---|
| 85 |
message when it arrives. |
|---|
| 86 |
|
|---|
| 87 |
@raise SMTPBadRcpt: Raised if messages to the address are |
|---|
| 88 |
not to be accepted. |
|---|
| 89 |
""" |
|---|
| 90 |
|
|---|
| 91 |
def validateFrom(self, helo, origin): |
|---|
| 92 |
""" |
|---|
| 93 |
Validate the address from which the message originates. |
|---|
| 94 |
|
|---|
| 95 |
@type helo: C{(str, str)} |
|---|
| 96 |
@param helo: The argument to the HELO command and the client's IP |
|---|
| 97 |
address. |
|---|
| 98 |
|
|---|
| 99 |
@type origin: C{Address} |
|---|
| 100 |
@param origin: The address the message is from |
|---|
| 101 |
|
|---|
| 102 |
@rtype: C{Deferred} or C{Address} |
|---|
| 103 |
@return: C{origin} or a C{Deferred} whose callback will be |
|---|
| 104 |
passed C{origin}. |
|---|
| 105 |
|
|---|
| 106 |
@raise SMTPBadSender: Raised of messages from this address are |
|---|
| 107 |
not to be accepted. |
|---|
| 108 |
""" |
|---|
| 109 |
|
|---|
| 110 |
class IMessageDeliveryFactory(components.Interface): |
|---|
| 111 |
"""An alternate interface to implement for handling message delivery. |
|---|
| 112 |
|
|---|
| 113 |
It is useful to implement this interface instead of L{IMessageDelivery} |
|---|
| 114 |
directly because it allows the implementor to distinguish between |
|---|
| 115 |
different messages delivery over the same connection. This can be |
|---|
| 116 |
used to optimize delivery of a single message to multiple recipients, |
|---|
| 117 |
something which cannot be done by L{IMessageDelivery} implementors |
|---|
| 118 |
due to their lack of information. |
|---|
| 119 |
""" |
|---|
| 120 |
def getMessageDelivery(self): |
|---|
| 121 |
"""Return an L{IMessageDelivery} object. |
|---|
| 122 |
|
|---|
| 123 |
This will be called once per message. |
|---|
| 124 |
""" |
|---|
| 125 |
|
|---|
| 126 |
class SMTPError(Exception): |
|---|
| 127 |
pass |
|---|
| 128 |
|
|---|
| 129 |
class SMTPClientError(SMTPError): |
|---|
| 130 |
"""Base class for SMTP client errors. |
|---|
| 131 |
""" |
|---|
| 132 |
def __init__(self, code, resp, log=None, addresses=None, isFatal=False, retry=False): |
|---|
| 133 |
""" |
|---|
| 134 |
@param code: The SMTP response code associated with this error. |
|---|
| 135 |
@param resp: The string response associated with this error. |
|---|
| 136 |
@param log: A string log of the exchange leading up to and including the error. |
|---|
| 137 |
|
|---|
| 138 |
@param isFatal: A boolean indicating whether this connection can proceed |
|---|
| 139 |
or not. If True, the connection will be dropped. |
|---|
| 140 |
@param retry: A boolean indicating whether the delivery should be retried. |
|---|
| 141 |
If True and the factory indicates further retries are desirable, they will |
|---|
| 142 |
be attempted, otherwise the delivery will be failed. |
|---|
| 143 |
""" |
|---|
| 144 |
self.code = code |
|---|
| 145 |
self.resp = resp |
|---|
| 146 |
self.log = log |
|---|
| 147 |
self.addresses = addresses |
|---|
| 148 |
self.isFatal = isFatal |
|---|
| 149 |
self.retry = retry |
|---|
| 150 |
|
|---|
| 151 |
def __str__(self): |
|---|
| 152 |
if self.code > 0: |
|---|
| 153 |
res = ["%.3d %s" % (self.code, self.resp)] |
|---|
| 154 |
else: |
|---|
| 155 |
res = [self.resp] |
|---|
| 156 |
if self.log: |
|---|
| 157 |
res.append('') |
|---|
| 158 |
res.append(self.log) |
|---|
| 159 |
return '\n'.join(res) |
|---|
| 160 |
|
|---|
| 161 |
|
|---|
| 162 |
class ESMTPClientError(SMTPClientError): |
|---|
| 163 |
"""Base class for ESMTP client errors. |
|---|
| 164 |
""" |
|---|
| 165 |
|
|---|
| 166 |
class EHLORequiredError(ESMTPClientError): |
|---|
| 167 |
"""The server does not support EHLO. |
|---|
| 168 |
|
|---|
| 169 |
This is considered a non-fatal error (the connection will not be |
|---|
| 170 |
dropped). |
|---|
| 171 |
""" |
|---|
| 172 |
|
|---|
| 173 |
class AUTHRequiredError(ESMTPClientError): |
|---|
| 174 |
"""Authentication was required but the server does not support it. |
|---|
| 175 |
|
|---|
| 176 |
This is considered a non-fatal error (the connection will not be |
|---|
| 177 |
dropped). |
|---|
| 178 |
""" |
|---|
| 179 |
|
|---|
| 180 |
class TLSRequiredError(ESMTPClientError): |
|---|
| 181 |
"""Transport security was required but the server does not support it. |
|---|
| 182 |
|
|---|
| 183 |
This is considered a non-fatal error (the connection will not be |
|---|
| 184 |
dropped). |
|---|
| 185 |
""" |
|---|
| 186 |
|
|---|
| 187 |
class AUTHDeclinedError(ESMTPClientError): |
|---|
| 188 |
"""The server rejected our credentials. |
|---|
| 189 |
|
|---|
| 190 |
Either the username, password, or challenge response |
|---|
| 191 |
given to the server was rejected. |
|---|
| 192 |
|
|---|
| 193 |
This is considered a non-fatal error (the connection will not be |
|---|
| 194 |
dropped). |
|---|
| 195 |
""" |
|---|
| 196 |
|
|---|
| 197 |
class AuthenticationError(ESMTPClientError): |
|---|
| 198 |
"""An error ocurred while authenticating. |
|---|
| 199 |
|
|---|
| 200 |
Either the server rejected our request for authentication or the |
|---|
| 201 |
challenge received was malformed. |
|---|
| 202 |
|
|---|
| 203 |
This is considered a non-fatal error (the connection will not be |
|---|
| 204 |
dropped). |
|---|
| 205 |
""" |
|---|
| 206 |
|
|---|
| 207 |
class TLSError(ESMTPClientError): |
|---|
| 208 |
"""An error occurred while negiotiating for transport security. |
|---|
| 209 |
|
|---|
| 210 |
This is considered a non-fatal error (the connection will not be |
|---|
| 211 |
dropped). |
|---|
| 212 |
""" |
|---|
| 213 |
|
|---|
| 214 |
class SMTPConnectError(SMTPClientError): |
|---|
| 215 |
"""Failed to connect to the mail exchange host. |
|---|
| 216 |
|
|---|
| 217 |
This is considered a fatal error. A retry will be made. |
|---|
| 218 |
""" |
|---|
| 219 |
def __init__(self, code, resp, log=None, addresses=None, isFatal=True, retry=True): |
|---|
| 220 |
SMTPClientError.__init__(self, code, resp, log, addresses, isFatal, retry) |
|---|
| 221 |
|
|---|
| 222 |
class SMTPTimeoutError(SMTPClientError): |
|---|
| 223 |
"""Failed to receive a response from the server in the expected time period. |
|---|
| 224 |
|
|---|
| 225 |
This is considered a fatal error. A retry will be made. |
|---|
| 226 |
""" |
|---|
| 227 |
def __init__(self, code, resp, log=None, addresses=None, isFatal=True, retry=True): |
|---|
| 228 |
SMTPClientError.__init__(self, code, resp, log, addresses, isFatal, retry) |
|---|
| 229 |
|
|---|
| 230 |
class SMTPProtocolError(SMTPClientError): |
|---|
| 231 |
"""The server sent a mangled response. |
|---|
| 232 |
|
|---|
| 233 |
This is considered a fatal error. A retry will not be made. |
|---|
| 234 |
""" |
|---|
| 235 |
def __init__(self, code, resp, log=None, addresses=None, isFatal=True, retry=False): |
|---|
| 236 |
SMTPClientError.__init__(self, code, resp, log, addresses, isFatal, retry) |
|---|
| 237 |
|
|---|
| 238 |
class SMTPDeliveryError(SMTPClientError): |
|---|
| 239 |
"""Indicates that a delivery attempt has had an error. |
|---|
| 240 |
""" |
|---|
| 241 |
|
|---|
| 242 |
class SMTPServerError(SMTPError): |
|---|
| 243 |
def __init__(self, code, resp): |
|---|
| 244 |
self.code = code |
|---|
| 245 |
self.resp = resp |
|---|
| 246 |
|
|---|
| 247 |
def __str__(self): |
|---|
| 248 |
return "%.3d %s" % (self.code, self.resp) |
|---|
| 249 |
|
|---|
| 250 |
class SMTPAddressError(SMTPServerError): |
|---|
| 251 |
def __init__(self, addr, code, resp): |
|---|
| 252 |
SMTPServerError.__init__(self, code, resp) |
|---|
| 253 |
self.addr = Address(addr) |
|---|
| 254 |
|
|---|
| 255 |
def __str__(self): |
|---|
| 256 |
return "%.3d <%s>... %s" % (self.code, self.addr, self.resp) |
|---|
| 257 |
|
|---|
| 258 |
class SMTPBadRcpt(SMTPAddressError): |
|---|
| 259 |
def __init__(self, addr, code=550, |
|---|
| 260 |
resp='Cannot receive for specified address'): |
|---|
| 261 |
SMTPAddressError.__init__(self, addr, code, resp) |
|---|
| 262 |
|
|---|
| 263 |
class SMTPBadSender(SMTPAddressError): |
|---|
| 264 |
def __init__(self, addr, code=550, resp='Sender not acceptable'): |
|---|
| 265 |
SMTPAddressError.__init__(self, addr, code, resp) |
|---|
| 266 |
|
|---|
| 267 |
def rfc822date(timeinfo=None,local=1): |
|---|
| 268 |
""" |
|---|
| 269 |
Format an RFC-2822 compliant date string. |
|---|
| 270 |
|
|---|
| 271 |
@param timeinfo: (optional) A sequence as returned by C{time.localtime()} |
|---|
| 272 |
or C{time.gmtime()}. Default is now. |
|---|
| 273 |
@param local: (optional) Indicates if the supplied time is local or |
|---|
| 274 |
universal time, or if no time is given, whether now should be local or |
|---|
| 275 |
universal time. Default is local, as suggested (SHOULD) by rfc-2822. |
|---|
| 276 |
|
|---|
| 277 |
@returns: A string representing the time and date in RFC-2822 format. |
|---|
| 278 |
""" |
|---|
| 279 |
if not timeinfo: |
|---|
| 280 |
if local: |
|---|
| 281 |
timeinfo = time.localtime() |
|---|
| 282 |
else: |
|---|
| 283 |
timeinfo = time.gmtime() |
|---|
| 284 |
if local: |
|---|
| 285 |
if timeinfo[8]: |
|---|
| 286 |
|
|---|
| 287 |
tz = -time.altzone |
|---|
| 288 |
else: |
|---|
| 289 |
tz = -time.timezone |
|---|
| 290 |
|
|---|
| 291 |
(tzhr, tzmin) = divmod(abs(tz), 3600) |
|---|
| 292 |
if tz: |
|---|
| 293 |
tzhr *= int(abs(tz)/tz) |
|---|
| 294 |
(tzmin, tzsec) = divmod(tzmin, 60) |
|---|
| 295 |
else: |
|---|
| 296 |
(tzhr, tzmin) = (0,0) |
|---|
| 297 |
|
|---|
| 298 |
return "%s, %02d %s %04d %02d:%02d:%02d %+03d%02d" % ( |
|---|
| 299 |
['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'][timeinfo[6]], |
|---|
| 300 |
timeinfo[2], |
|---|
| 301 |
['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', |
|---|
| 302 |
'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'][timeinfo[1] - 1], |
|---|
| 303 |
timeinfo[0], timeinfo[3], timeinfo[4], timeinfo[5], |
|---|
| 304 |
tzhr, tzmin) |
|---|
| 305 |
|
|---|
| 306 |
def idGenerator(): |
|---|
| 307 |
i = 0 |
|---|
| 308 |
while True: |
|---|
| 309 |
yield i |
|---|
| 310 |
i += 1 |
|---|
| 311 |
|
|---|
| 312 |
def messageid(uniq=None, N=idGenerator().next): |
|---|
| 313 |
"""Return a globally unique random string in RFC 2822 Message-ID format |
|---|
| 314 |
|
|---|
| 315 |
<datetime.pid.random@host.dom.ain> |
|---|
| 316 |
|
|---|
| 317 |
Optional uniq string will be added to strenghten uniqueness if given. |
|---|
| 318 |
""" |
|---|
| 319 |
datetime = time.strftime('%Y%m%d%H%M%S', time.gmtime()) |
|---|
| 320 |
pid = os.getpid() |
|---|
| 321 |
rand = random.randrange(2**31L-1) |
|---|
| 322 |
if uniq is None: |
|---|
| 323 |
uniq = '' |
|---|
| 324 |
else: |
|---|
| 325 |
uniq = '.' + uniq |
|---|
| 326 |
|
|---|
| 327 |
return '<%s.%s.%s%s.%s@%s>' % (datetime, pid, rand, uniq, N(), DNSNAME) |
|---|
| 328 |
|
|---|
| 329 |
def quoteaddr(addr): |
|---|
| 330 |
"""Turn an email address, possibly with realname part etc, into |
|---|
| 331 |
a form suitable for and SMTP envelope. |
|---|
| 332 |
""" |
|---|
| 333 |
|
|---|
| 334 |
if isinstance(addr, Address): |
|---|
| 335 |
return '<%s>' % str(addr) |
|---|
| 336 |
|
|---|
| 337 |
res = rfc822.parseaddr(addr) |
|---|
| 338 |
|
|---|
| 339 |
if res == (None, None): |
|---|
| 340 |
|
|---|
| 341 |
return '<%s>' % str(addr) |
|---|
| 342 |
else: |
|---|
| 343 |
return '<%s>' % str(res[1]) |
|---|
| 344 |
|
|---|
| 345 |
COMMAND, DATA, AUTH = 'COMMAND', 'DATA', 'AUTH' |
|---|
| 346 |
|
|---|
| 347 |
class AddressError(SMTPError): |
|---|
| 348 |
"Parse error in address" |
|---|
| 349 |
|
|---|
| 350 |
|
|---|
| 351 |
atom = r"[-A-Za-z0-9!\#$%&'*+/=?^_`{|}~]" |
|---|
| 352 |
|
|---|
| 353 |
class Address: |
|---|
| 354 |
"""Parse and hold an RFC 2821 address. |
|---|
| 355 |
|
|---|
| 356 |
Source routes are stipped and ignored, UUCP-style bang-paths |
|---|
| 357 |
and %-style routing are not parsed. |
|---|
| 358 |
|
|---|
| 359 |
@type domain: C{str} |
|---|
| 360 |
@ivar domain: The domain within which this address resides. |
|---|
| 361 |
|
|---|
| 362 |
@type local: C{str} |
|---|
| 363 |
@ivar local: The local (\"user\") portion of this address. |
|---|
| 364 |
""" |
|---|
| 365 |
|
|---|
| 366 |
tstring = re.compile(r'''( # A string of |
|---|
| 367 |
(?:"[^"]*" # quoted string |
|---|
| 368 |
|\\. # backslash-escaped characted |
|---|
| 369 |
|''' + atom + r''' # atom character |
|---|
| 370 |
)+|.) # or any single character''',re.X) |
|---|
| 371 |
atomre = re.compile(atom) |
|---|
| 372 |
|
|---|
| 373 |
def __init__(self, addr, defaultDomain=None): |
|---|
| 374 |
if isinstance(addr, User): |
|---|
| 375 |
addr = addr.dest |
|---|
| 376 |
if isinstance(addr, Address): |
|---|
| 377 |
self.__dict__ = addr.__dict__.copy() |
|---|
| 378 |
return |
|---|
| 379 |
elif not isinstance(addr, types.StringTypes): |
|---|
| 380 |
addr = str(addr) |
|---|
| 381 |
self.addrstr = addr |
|---|
| 382 |
|
|---|
| 383 |
|
|---|
| 384 |
atl = filter(None,self.tstring.split(addr)) |
|---|
| 385 |
|
|---|
| 386 |
local = [] |
|---|
| 387 |
domain = [] |
|---|
| 388 |
|
|---|
| 389 |
while atl: |
|---|
| 390 |
if atl[0] == '<': |
|---|
| 391 |
if atl[-1] != '>': |
|---|
| 392 |
raise AddressError, "Unbalanced <>" |
|---|
| 393 |
atl = atl[1:-1] |
|---|
| 394 |
elif atl[0] == '@': |
|---|
| 395 |
atl = atl[1:] |
|---|
| 396 |
if not local: |
|---|
| 397 |
|
|---|
| 398 |
while atl and atl[0] != ':': |
|---|
| 399 |
|
|---|
| 400 |
atl = atl[1:] |
|---|
| 401 |
if not atl: |
|---|
| 402 |
raise AddressError, "Malformed source route" |
|---|
| 403 |
atl = atl[1:] |
|---|
| 404 |
elif domain: |
|---|
| 405 |
raise AddressError, "Too many @" |
|---|
| 406 |
else: |
|---|
| 407 |
|
|---|
| 408 |
domain = [''] |
|---|
| 409 |
elif len(atl[0]) == 1 and not self.atomre.match(atl[0]) and atl[0] != '.': |
|---|
| 410 |
raise AddressError, "Parse error at %r of %r" % (atl[0], (addr, atl)) |
|---|
| 411 |
else: |
|---|
| 412 |
if not domain: |
|---|
| 413 |
local.append(atl[0]) |
|---|
| 414 |
else: |
|---|
| 415 |
domain.append(atl[0]) |
|---|
| 416 |
atl = atl[1:] |
|---|
| 417 |
|
|---|
| 418 |
self.local = ''.join(local) |
|---|
| 419 |
self.domain = ''.join(domain) |
|---|
| 420 |
if self.local != '' and self.domain == '': |
|---|
| 421 |
if defaultDomain is None: |
|---|
| 422 |
defaultDomain = DNSNAME |
|---|
| 423 |
self.domain = defaultDomain |
|---|
| 424 |
|
|---|
| 425 |
dequotebs = re.compile(r'\\(.)') |
|---|
| 426 |
|
|---|
| 427 |
def dequote(self,addr): |
|---|
| 428 |
"""Remove RFC-2821 quotes from address.""" |
|---|
| 429 |
res = [] |
|---|
| 430 |
|
|---|
| 431 |
atl = filter(None,self.tstring.split(str(addr))) |
|---|
| 432 |
|
|---|
| 433 |
for t in atl: |
|---|
| 434 |
if t[0] == '"' and t[-1] == '"': |
|---|
| 435 |
res.append(t[1:-1]) |
|---|
| 436 |
elif '\\' in t: |
|---|
| 437 |
res.append(self.dequotebs.sub(r'\1',t)) |
|---|
| 438 |
else: |
|---|
| 439 |
res.append(t) |
|---|
| 440 |
|
|---|
| 441 |
return ''.join(res) |
|---|
| 442 |
|
|---|
| 443 |
def __str__(self): |
|---|
| 444 |
if self.local or self.domain: |
|---|
| 445 |
return '@'.join((self.local, self.domain)) |
|---|
| 446 |
else: |
|---|
| 447 |
return '' |
|---|
| 448 |
|
|---|
| 449 |
def __repr__(self): |
|---|
| 450 |
return "%s.%s(%s)" % (self.__module__, self.__class__.__name__, |
|---|
| 451 |
repr(str(self))) |
|---|
| 452 |
|
|---|
| 453 |
class User: |
|---|
| 454 |
"""Hold information about and SMTP message recipient, |
|---|
| 455 |
including information on where the message came from |
|---|
| 456 |
""" |
|---|
| 457 |
|
|---|
| 458 |
def __init__(self, destination, helo, protocol, orig): |
|---|
| 459 |
host = getattr(protocol, 'host', None) |
|---|
| 460 |
self.dest = Address(destination, host) |
|---|
| 461 |
self.helo = helo |
|---|
| 462 |
self.protocol = protocol |
|---|
| 463 |
if isinstance(orig, Address): |
|---|
| 464 |
self.orig = orig |
|---|
| 465 |
else: |
|---|
| 466 |
self.orig = Address(orig, host) |
|---|
| 467 |
|
|---|
| 468 |
def __getstate__(self): |
|---|
| 469 |
"""Helper for pickle. |
|---|
| 470 |
|
|---|
| 471 |
protocol isn't picklabe, but we want User to be, so skip it in |
|---|
| 472 |
the pickle. |
|---|
| 473 |
""" |
|---|
| 474 |
return { 'dest' : self.dest, |
|---|
| 475 |
'helo' : self.helo, |
|---|
| 476 |
'protocol' : None, |
|---|
| 477 |
'orig' : self.orig } |
|---|
| 478 |
|
|---|
| 479 |
def __str__(self): |
|---|
| 480 |
return str(self.dest) |
|---|
| 481 |
|
|---|
| 482 |
class IMessage(components.Interface): |
|---|
| 483 |
"""Interface definition for messages that can be sent via SMTP.""" |
|---|
| 484 |
|
|---|
| 485 |
def lineReceived(self, line): |
|---|
| 486 |
"""handle another line""" |
|---|
| 487 |
|
|---|
| 488 |
def eomReceived(self): |
|---|
| 489 |
"""handle end of message |
|---|
| 490 |
|
|---|
| 491 |
return a deferred. The deferred should be called with either: |
|---|
| 492 |
callback(string) or errback(error) |
|---|
| 493 |
""" |
|---|
| 494 |
|
|---|
| 495 |
def connectionLost(self): |
|---|
| 496 |
"""handle message truncated |
|---|
| 497 |
|
|---|
| 498 |
semantics should be to discard the message |
|---|
| 499 |
""" |
|---|
| 500 |
|
|---|
| 501 |
class SMTP(basic.LineReceiver, policies.TimeoutMixin): |
|---|
| 502 |
"""SMTP server-side protocol.""" |
|---|
| 503 |
|
|---|
| 504 |
timeout = 600 |
|---|
| 505 |
host = DNSNAME |
|---|
| 506 |
portal = None |
|---|
| 507 |
|
|---|
| 508 |
|
|---|
| 509 |
noisy = True |
|---|
| 510 |
|
|---|
| 511 |
|
|---|
| 512 |
|
|---|
| 513 |
|
|---|
| 514 |
|
|---|
| 515 |
|
|---|
| 516 |
deliveryFactory = None |
|---|
| 517 |
|
|---|
| 518 |
|
|---|
| 519 |
|
|---|
| 520 |
|
|---|
| 521 |
|
|---|
| 522 |
|
|---|
| 523 |
delivery = None |
|---|
| 524 |
|
|---|
| 525 |
|
|---|
| 526 |
_onLogout = None |
|---|
| 527 |
|
|---|
| 528 |
def __init__(self, delivery=None, deliveryFactory=None): |
|---|
| 529 |
self.mode = COMMAND |
|---|
| 530 |
self._from = None |
|---|
| 531 |
self._helo = None |
|---|
| 532 |
self._to = [] |
|---|
| 533 |
self.delivery = delivery |
|---|
| 534 |
self.deliveryFactory = deliveryFactory |
|---|
| 535 |
|
|---|
| 536 |
def timeoutConnection(self): |
|---|
| 537 |
msg = '%s Timeout. Try talking faster next time!' % (self.host,) |
|---|
| 538 |
self.sendCode(421, msg) |
|---|
| 539 |
self.transport.loseConnection() |
|---|
| 540 |
|
|---|
| 541 |
def greeting(self): |
|---|
| 542 |
return '%s NO UCE NO UBE NO RELAY PROBES ESMTP' % (self.host,) |
|---|
| 543 |
|
|---|
| 544 |
def connectionMade(self): |
|---|
| 545 |
|
|---|
| 546 |
peer = self.transport.getPeer() |
|---|
| 547 |
try: |
|---|
| 548 |
host = peer.host |
|---|
| 549 |
except AttributeError: |
|---|
| 550 |
host = str(peer) |
|---|
| 551 |
self._helo = (None, host) |
|---|
| 552 |
self.sendCode(220, self.greeting()) |
|---|
| 553 |
self.setTimeout(self.timeout) |
|---|
| 554 |
|
|---|
| 555 |
def sendCode(self, code, message=''): |
|---|
| 556 |
"Send an SMTP code with a message." |
|---|
| 557 |
lines = message.splitlines() |
|---|
| 558 |
lastline = lines[-1:] |
|---|
| 559 |
for line in lines[:-1]: |
|---|
| 560 |
self.sendLine('%3.3d-%s' % (code, line)) |
|---|
| 561 |
self.sendLine('%3.3d %s' % (code, |
|---|
| 562 |
lastline and lastline[0] or '')) |
|---|
| 563 |
|
|---|
| 564 |
def lineReceived(self, line): |
|---|
| 565 |
self.resetTimeout() |
|---|
| 566 |
return getattr(self, 'state_' + self.mode)(line) |
|---|
| 567 |
|
|---|
| 568 |
def state_COMMAND(self, line): |
|---|
| 569 |
|
|---|
| 570 |
|
|---|
| 571 |
|
|---|
| 572 |
line = line.strip() |
|---|
| 573 |
|
|---|
| 574 |
parts = line.split(None, 1) |
|---|
| 575 |
if parts: |
|---|
| 576 |
method = self.lookupMethod(parts[0]) or self.do_UNKNOWN |
|---|
| 577 |
if len(parts) == 2: |
|---|
| 578 |
method(parts[1]) |
|---|
| 579 |
else: |
|---|
| 580 |
method('') |
|---|
| 581 |
else: |
|---|
| 582 |
self.sendSyntaxError() |
|---|
| 583 |
|
|---|
| 584 |
def sendSyntaxError(self): |
|---|
| 585 |
self.sendCode(500, 'Error: bad syntax') |
|---|
| 586 |
|
|---|
| 587 |
def lookupMethod(self, command): |
|---|
| 588 |
return getattr(self, 'do_' + command.upper(), None) |
|---|
| 589 |
|
|---|
| 590 |
def lineLengthExceeded(self, line): |
|---|
| 591 |
if self.mode is DATA: |
|---|
| 592 |
for message in self.__messages: |
|---|
| 593 |
message.connectionLost() |
|---|
| 594 |
self.mode = COMMAND |
|---|
| 595 |
del self.__messages |
|---|
| 596 |
self.sendCode(500, 'Line too long') |
|---|
| 597 |
|
|---|
| 598 |
def rawDataReceived(self, data): |
|---|
| 599 |
"""Throw away rest of long line""" |
|---|
| 600 |
rest = string.split(data, '\r\n', 1) |
|---|
| 601 |
if len(rest) == 2: |
|---|
| 602 |
self.setLineMode(rest[1]) |
|---|
| 603 |
|
|---|
| 604 |
def do_UNKNOWN(self, rest): |
|---|
| 605 |
self.sendCode(500, 'Command not implemented') |
|---|
| 606 |
|
|---|
| 607 |
def do_HELO(self, rest): |
|---|
| 608 |
peer = self.transport.getPeer() |
|---|
| 609 |
try: |
|---|
| 610 |
host = peer.host |
|---|
| 611 |
except AttributeError: |
|---|
| 612 |
host = str(peer) |
|---|
| 613 |
self._helo = (rest, host) |
|---|
| 614 |
self._from = None |
|---|
| 615 |
self._to = [] |
|---|
| 616 |
self.sendCode(250, '%s Hello %s, nice to meet you' % (self.host, host)) |
|---|
| 617 |
|
|---|
| 618 |
def do_QUIT(self, rest): |
|---|
| 619 |
self.sendCode(221, 'See you later') |
|---|
| 620 |
self.transport.loseConnection() |
|---|
| 621 |
|
|---|
| 622 |
|
|---|
| 623 |
|
|---|
| 624 |
qstring = r'("[^"]*"|\\.|' + atom + r'|[@.,:])+' |
|---|
| 625 |
|
|---|
| 626 |
mail_re = re.compile(r'''\s*FROM:\s*(?P<path><> # Empty <> |
|---|
| 627 |
|<''' + qstring + r'''> # <addr> |
|---|
| 628 |
|''' + qstring + r''' # addr |
|---|
| 629 |
)\s*(\s(?P<opts>.*))? # Optional WS + ESMTP options |
|---|
| 630 |
$''',re.I|re.X) |
|---|
| 631 |
rcpt_re = re.compile(r'\s*TO:\s*(?P<path><' + qstring + r'''> # <addr> |
|---|
| 632 |
|''' + qstring + r''' # addr |
|---|
| 633 |
)\s*(\s(?P<opts>.*))? # Optional WS + ESMTP options |
|---|
| 634 |
$''',re.I|re.X) |
|---|
| 635 |
|
|---|
| 636 |
def do_MAIL(self, rest): |
|---|
| 637 |
if self._from: |
|---|
| 638 |
self.sendCode(503,"Only one sender per message, please") |
|---|
| 639 |
return |
|---|
| 640 |
|
|---|
| 641 |
self._to = [] |
|---|
| 642 |
m = self.mail_re.match(rest) |
|---|
| 643 |
if not m: |
|---|
| 644 |
self.sendCode(501, "Syntax error") |
|---|
| 645 |
return |
|---|
| 646 |
|
|---|
| 647 |
try: |
|---|
| 648 |
addr = Address(m.group('path'), self.host) |
|---|
| 649 |
except AddressError, e: |
|---|
| 650 |
self.sendCode(553, str(e)) |
|---|
| 651 |
return |
|---|
| 652 |
|
|---|
| 653 |
defer.maybeDeferred(self.validateFrom, self._helo, addr |
|---|
| 654 |
).addCallbacks(self._cbFromValidate, self._ebFromValidate |
|---|
| 655 |
) |
|---|
| 656 |
|
|---|
| 657 |
def _cbFromValidate(self, from_, code=250, msg='Sender address accepted'): |
|---|
| 658 |
self._from = from_ |
|---|
| 659 |
self.sendCode(code, msg) |
|---|
| 660 |
|
|---|
| 661 |
def _ebFromValidate(self, failure): |
|---|
| 662 |
if failure.check(SMTPBadSender): |
|---|
| 663 |
self.sendCode(failure.value.code, |
|---|
| 664 |
'Cannot receive for specified address %s: %s' |
|---|
| 665 |
% (quoteaddr(failure.value.addr), failure.value.resp)) |
|---|
| 666 |
elif failure.check(SMTPServerError): |
|---|
| 667 |
self.sendCode(failure.value.code, failure.value.resp) |
|---|
| 668 |
else: |
|---|
| 669 |
log.err(failure) |
|---|
| 670 |
self.sendCode( |
|---|
| 671 |
451, |
|---|
| 672 |
'Requested action aborted: local error in processing' |
|---|
| 673 |
) |
|---|
| 674 |
|
|---|
| 675 |
|
|---|
| 676 |
def do_RCPT(self, rest): |
|---|
| 677 |
if not self._from: |
|---|
| 678 |
self.sendCode(503, "Must have sender before recipient") |
|---|
| 679 |
return |
|---|
| 680 |
m = self.rcpt_re.match(rest) |
|---|
| 681 |
if not m: |
|---|
| 682 |
self.sendCode(501, "Syntax error") |
|---|
| 683 |
return |
|---|
| 684 |
|
|---|
| 685 |
try: |
|---|
| 686 |
user = User(m.group('path'), self._helo, self, self._from) |
|---|
| 687 |
except AddressError, e: |
|---|
| 688 |
self.sendCode(553, str(e)) |
|---|
| 689 |
return |
|---|
| 690 |
|
|---|
| 691 |
d = defer.maybeDeferred(self.validateTo, user) |
|---|
| 692 |
d.addCallbacks( |
|---|
| 693 |
self._cbToValidate, |
|---|
| 694 |
self._ebToValidate, |
|---|
| 695 |
callbackArgs=(user,) |
|---|
| 696 |
) |
|---|
| 697 |
|
|---|
| 698 |
def _cbToValidate(self, to, user=None, code=250, msg='Recipient address accepted'): |
|---|
| 699 |
if user is None: |
|---|
| 700 |
user = to |
|---|
| 701 |
self._to.append((user, to)) |
|---|
| 702 |
self.sendCode(code, msg) |
|---|
| 703 |
|
|---|
| 704 |
def _ebToValidate(self, failure): |
|---|
| 705 |
if failure.check(SMTPBadRcpt, SMTPServerError): |
|---|
| 706 |
self.sendCode(failure.value.code, failure.value.resp) |
|---|
| 707 |
else: |
|---|
| 708 |
log.err(failure) |
|---|
| 709 |
self.sendCode( |
|---|
| 710 |
451, |
|---|
| 711 |
'Requested action aborted: local error in processing' |
|---|
| 712 |
) |
|---|
| 713 |
|
|---|
| 714 |
def do_DATA(self, rest): |
|---|
| 715 |
if self._from is None or (not self._to): |
|---|
| 716 |
self.sendCode(503, 'Must have valid receiver and originator') |
|---|
| 717 |
return |
|---|
| 718 |
assert self.delivery |
|---|
| 719 |
self.mode = DATA |
|---|
| 720 |
helo, origin = self._helo, self._from |
|---|
| 721 |
recipients = self._to |
|---|
| 722 |
|
|---|
| 723 |
self._from = None |
|---|
| 724 |
self._to = [] |
|---|
| 725 |
self.datafailed = None |
|---|
| 726 |
|
|---|
| 727 |
try: |
|---|
| 728 |
self.__messages = [f() for (u, f) in recipients] |
|---|
| 729 |
except SMTPServerError, e: |
|---|
| 730 |
self.sendCode(e.code, e.resp) |
|---|
| 731 |
self.mode = COMMAND |
|---|
| 732 |
return |
|---|
| 733 |
except: |
|---|
| 734 |
log.err() |
|---|
| 735 |
self.sendCode(550, "Internal server error") |
|---|
| 736 |
self.mode = COMMAND |
|---|
| 737 |
return |
|---|
| 738 |
|
|---|
| 739 |
rcvdhdr = self.delivery.receivedHeader( |
|---|
| 740 |
helo, origin, [u for (u, f) in recipients]) |
|---|
| 741 |
|
|---|
| 742 |
self.__inheader = self.__inbody = 0 |
|---|
| 743 |
if rcvdhdr: |
|---|
| 744 |
try: |
|---|
| 745 |
for message in self.__messages: |
|---|
| 746 |
message.lineReceived(rcvdhdr) |
|---|
| 747 |
except SMTPServerError, e: |
|---|
| 748 |
self.sendCode(e.code, e.resp) |
|---|
| 749 |
self.mode = COMMAND |
|---|
| 750 |
return |
|---|
| 751 |
self.sendCode(354, 'Continue') |
|---|
| 752 |
if self.noisy: |
|---|
| 753 |
fmt = 'Receiving message for delivery: from=%s to=%s' |
|---|
| 754 |
log.msg(fmt % (origin, [str(u) for (u, f) in recipients])) |
|---|
| 755 |
|
|---|
| 756 |
def connectionLost(self, reason): |
|---|
| 757 |
|
|---|
| 758 |
|
|---|
| 759 |
|
|---|
| 760 |
|
|---|
| 761 |
if self.mode is DATA: |
|---|
| 762 |
try: |
|---|
| 763 |
for message in self.__messages: |
|---|
| 764 |
try: |
|---|
| 765 |
message.connectionLost() |
|---|
| 766 |
except: |
|---|
| 767 |
log.err() |
|---|
| 768 |
del self.__messages |
|---|
| 769 |
except AttributeError: |
|---|
| 770 |
pass |
|---|
| 771 |
if self._onLogout: |
|---|
| 772 |
self._onLogout() |
|---|
| 773 |
self._onLogout = None |
|---|
| 774 |
self.setTimeout(None) |
|---|
| 775 |
|
|---|
| 776 |
def do_RSET(self, rest): |
|---|
| 777 |
self._from = None |
|---|
| 778 |
self._to = [] |
|---|
| 779 |
self.sendCode(250, 'I remember nothing.') |
|---|
| 780 |
|
|---|
| 781 |
def dataLineReceived(self, line): |
|---|
| 782 |
if line[:1] == '.': |
|---|
| 783 |
if line == '.': |
|---|
| 784 |
self.mode = COMMAND |
|---|
| 785 |
if self.datafailed: |
|---|
| 786 |
self.sendCode(self.datafailed.code, |
|---|
| 787 |
self.datafailed.resp) |
|---|
| 788 |
return |
|---|
| 789 |
if not self.__messages: |
|---|
| 790 |
self._messageHandled("thrown away") |
|---|
| 791 |
return |
|---|
| 792 |
defer.DeferredList([ |
|---|
| 793 |
m.eomReceived() for m in self.__messages |
|---|
| 794 |
], consumeErrors=True).addCallback(self._messageHandled |
|---|
| 795 |
) |
|---|
| 796 |
del self.__messages |
|---|
| 797 |
return |
|---|
| 798 |
line = line[1:] |
|---|
| 799 |
|
|---|
| 800 |
if self.datafailed: |
|---|
| 801 |
return |
|---|
| 802 |
|
|---|
| 803 |
try: |
|---|
| 804 |
|
|---|
| 805 |
|
|---|
| 806 |
|
|---|
| 807 |
if not self.__inheader and not self.__inbody: |
|---|
| 808 |
if ':' in line: |
|---|
| 809 |
self.__inheader = 1 |
|---|
| 810 |
elif line: |
|---|
| 811 |
for message in self.__messages: |
|---|
| 812 |
message.lineReceived('') |
|---|
| 813 |
self.__inbody = 1 |
|---|
| 814 |
|
|---|
| 815 |
if not line: |
|---|
| 816 |
self.__inbody = 1 |
|---|
| 817 |
|
|---|
| 818 |
for message in self.__messages: |
|---|
| 819 |
message.lineReceived(line) |
|---|
| 820 |
except SMTPServerError, e: |
|---|
| 821 |
self.datafailed = e |
|---|
| 822 |
for message in self.__messages: |
|---|
| 823 |
message.connectionLost() |
|---|
| 824 |
state_DATA = dataLineReceived |
|---|
| 825 |
|
|---|
| 826 |
def _messageHandled(self, resultList): |
|---|
| 827 |
failures = 0 |
|---|
| 828 |
for (success, result) in resultList: |
|---|
| 829 |
if not success: |
|---|
| 830 |
failures += 1 |
|---|
| 831 |
log.err(result) |
|---|
| 832 |
if failures: |
|---|
| 833 |
msg = 'Could not send e-mail' |
|---|
| 834 |
L = len(resultList) |
|---|
| 835 |
if L > 1: |
|---|
| 836 |
msg += ' (%d failures out of %d recipients)' % (failures, L) |
|---|
| 837 |
self.sendCode(550, msg) |
|---|
| 838 |
else: |
|---|
| 839 |
self.sendCode(250, 'Delivery in progress') |
|---|
| 840 |
|
|---|
| 841 |
def _cbAuthenticated(self, (iface, avatar, logout)): |
|---|
| 842 |
if issubclass(iface, IMessageDeliveryFactory): |
|---|
| 843 |
self.deliveryFactory = avatar |
|---|
| 844 |
self.delivery = None |
|---|
| 845 |
elif issubclass(iface, IMessageDelivery): |
|---|
| 846 |
self.deliveryFactory = None |
|---|
| 847 |
self.delivery = avatar |
|---|
| 848 |
else: |
|---|
| 849 |
raise RuntimeError("%s is not a supported interface" % (iface.__name__,)) |
|---|
| 850 |
self._onLogout = logout |
|---|
| 851 |
self.authenticated = 1 |
|---|
| 852 |
self.challenger = None |
|---|
| 853 |
|
|---|
| 854 |
def _ebAuthenticated(self, reason): |
|---|
| 855 |
self.challenge = None |
|---|
| 856 |
if reason.check(cred.error.UnauthorizedLogin): |
|---|
| 857 |
self.sendCode(535, 'Authentication failed') |
|---|
| 858 |
elif reason.check(SMTPAddressError): |
|---|
| 859 |
self.sendCode(reason.value.code, reason.value.resp) |
|---|
| 860 |
else: |
|---|
| 861 |
self.sendCode(451, 'Requested action aborted: local error in processing') |
|---|
| 862 |
log.err(reason) |
|---|
| 863 |
|
|---|
| 864 |
|
|---|
| 865 |
def validateFrom(self, helo, origin): |
|---|
| 866 |
""" |
|---|
| 867 |
Validate the address from which the message originates. |
|---|
| 868 |
|
|---|
| 869 |
@type helo: C{(str, str)} |
|---|
| 870 |
@param helo: The argument to the HELO command and the client's IP |
|---|
| 871 |
address. |
|---|
| 872 |
|
|---|
| 873 |
@type origin: C{Address} |
|---|
| 874 |
@param origin: The address the message is from |
|---|
| 875 |
|
|---|
| 876 |
@rtype: C{Deferred} or C{Address} |
|---|
| 877 |
@return: C{origin} or a C{Deferred} whose callback will be |
|---|
| 878 |
passed C{origin}. |
|---|
| 879 |
|
|---|
| 880 |
@raise SMTPBadSender: Raised of messages from this address are |
|---|
| 881 |
not to be accepted. |
|---|
| 882 |
""" |
|---|
| 883 |
if self.deliveryFactory is not None: |
|---|
| 884 |
self.delivery = self.deliveryFactory.getMessageDelivery() |
|---|
| 885 |
|
|---|
| 886 |
if self.delivery is not None: |
|---|
| 887 |
return defer.maybeDeferred(self.delivery.validateFrom, |
|---|
| 888 |
helo, origin) |
|---|
| 889 |
|
|---|
| 890 |
|
|---|
| 891 |
|
|---|
| 892 |
|
|---|
| 893 |
if self.portal: |
|---|
| 894 |
return self.portal.login(cred.credentials.Anonymous(), None, |
|---|
| 895 |
IMessageDeliveryFactory, IMessageDelivery |
|---|
| 896 |
).addCallback(self._cbAuthenticated |
|---|
| 897 |
).addCallback(lambda _: self.validateFrom(helo, origin) |
|---|
| 898 |
).addErrback(self._ebAuthenticated |
|---|
| 899 |
) |
|---|
| 900 |
raise SMTPBadSender(origin) |
|---|
| 901 |
|
|---|
| 902 |
def validateTo(self, user): |
|---|
| 903 |
""" |
|---|
| 904 |
Validate the address for which the message is destined. |
|---|
| 905 |
|
|---|
| 906 |
@type user: C{User} |
|---|
| 907 |
@param user: The address to validate. |
|---|
| 908 |
|
|---|
| 909 |
@rtype: no-argument callable |
|---|
| 910 |
@return: A C{Deferred} which becomes, or a callable which |
|---|
| 911 |
takes no arguments and returns an object implementing C{IMessage}. |
|---|
| 912 |
This will be called and the returned object used to deliver the |
|---|
| 913 |
message when it arrives. |
|---|
| 914 |
|
|---|
| 915 |
@raise SMTPBadRcpt: Raised if messages to the address are |
|---|
| 916 |
not to be accepted. |
|---|
| 917 |
""" |
|---|
| 918 |
if self.delivery: |
|---|
| 919 |
return self.delivery.validateTo(user) |
|---|
| 920 |
raise SMTPBadRcpt(user) |
|---|
| 921 |
|
|---|
| 922 |
def startMessage(self, recipients): |
|---|
| 923 |
if self.delivery: |
|---|
| 924 |
return self.delivery.startMessage(recipients) |
|---|
| 925 |
return [] |
|---|
| 926 |
|
|---|
| 927 |
|
|---|
| 928 |
class SMTPFactory(protocol.ServerFactory): |
|---|
| 929 |
"""Factory for SMTP.""" |
|---|
| 930 |
|
|---|
| 931 |
|
|---|
| 932 |
domain = DNSNAME |
|---|
| 933 |
timeout = 600 |
|---|
| 934 |
protocol = SMTP |
|---|
| 935 |
|
|---|
| 936 |
portal = None |
|---|
| 937 |
|
|---|
| 938 |
def __init__(self, portal = None): |
|---|
| 939 |
self.portal = portal |
|---|
| 940 |
|
|---|
| 941 |
def buildProtocol(self, addr): |
|---|
| 942 |
p = protocol.ServerFactory.buildProtocol(self, addr) |
|---|
| 943 |
p.portal = self.portal |
|---|
| 944 |
p.host = self.domain |
|---|
| 945 |
return p |
|---|
| 946 |
|
|---|
| 947 |
class SMTPClient(basic.LineReceiver, policies.TimeoutMixin): |
|---|
| 948 |
"""SMTP client for sending emails.""" |
|---|
| 949 |
|
|---|
| 950 |
|
|---|
| 951 |
debug = True |
|---|
| 952 |
|
|---|
| 953 |
|
|---|
| 954 |
|
|---|
| 955 |
timeout = None |
|---|
| 956 |
|
|---|
| 957 |
def __init__(self, identity, logsize=10): |
|---|
| 958 |
self.identity = identity or '' |
|---|
| 959 |
self.toAddressesResult = [] |
|---|
| 960 |
self.successAddresses = [] |
|---|
| 961 |
self._from = None |
|---|
| 962 |
self.resp = [] |
|---|
| 963 |
self.code = -1 |
|---|
| 964 |
self.log = util.LineLog(logsize) |
|---|
| 965 |
|
|---|
| 966 |
def sendLine(self, line): |
|---|
| 967 |
|
|---|
| 968 |
if self.debug: |
|---|
| 969 |
self.log.append('>>> ' + line) |
|---|
| 970 |
|
|---|
| 971 |
basic.LineReceiver.sendLine(self,line) |
|---|
| 972 |
|
|---|
| 973 |
def connectionMade(self): |
|---|
| 974 |
self.setTimeout(self.timeout) |
|---|
| 975 |
|
|---|
| 976 |
self._expected = [ 220 ] |
|---|
| 977 |
self._okresponse = self.smtpState_helo |
|---|
| 978 |
self._failresponse = self.smtpConnectionFailed |
|---|
| 979 |
|
|---|
| 980 |
def connectionLost(self, reason=protocol.connectionDone): |
|---|
| 981 |
"""We are no longer connected""" |
|---|
| 982 |
self.setTimeout(None) |
|---|
| 983 |
self.mailFile = None |
|---|
| 984 |
|
|---|
| 985 |
def timeoutConnection(self): |
|---|
| 986 |
self.sendError( |
|---|
| 987 |
SMTPTimeoutError(-1, |
|---|
| 988 |
"Timeout waiting for SMTP server response", |
|---|
| 989 |
self.log)) |
|---|
| 990 |
|
|---|
| 991 |
def lineReceived(self, line): |
|---|
| 992 |
self.resetTimeout() |
|---|
| 993 |
|
|---|
| 994 |
|
|---|
| 995 |
if self.debug: |
|---|
| 996 |
self.log.append('<<< ' + line) |
|---|
| 997 |
|
|---|
| 998 |
why = None |
|---|
| 999 |
|
|---|
| 1000 |
try: |
|---|
| 1001 |
self.code = int(line[:3]) |
|---|
| 1002 |
except ValueError: |
|---|
| 1003 |
|
|---|
| 1004 |
self.sendError(SMTPProtocolError(-1, "Invalid response from SMTP server: %s" % line, self.log.str())) |
|---|
| 1005 |
return |
|---|
| 1006 |
|
|---|
| 1007 |
if line[0] == '0': |
|---|
| 1008 |
|
|---|
| 1009 |
return |
|---|
| 1010 |
|
|---|
| 1011 |
self.resp.append(line[4:]) |
|---|
| 1012 |
|
|---|
| 1013 |
if line[3:4] == '-': |
|---|
| 1014 |
|
|---|
| 1015 |
return |
|---|
| 1016 |
|
|---|
| 1017 |
if self.code in self._expected: |
|---|
| 1018 |
why = self._okresponse(self.code,'\n'.join(self.resp)) |
|---|
| 1019 |
else: |
|---|
| 1020 |
why = self._failresponse(self.code,'\n'.join(self.resp)) |
|---|
| 1021 |
|
|---|
| 1022 |
self.code = -1 |
|---|
| 1023 |
self.resp = [] |
|---|
| 1024 |
return why |
|---|
| 1025 |
|
|---|
| 1026 |
def smtpConnectionFailed(self, code, resp): |
|---|
| 1027 |
self.sendError(SMTPConnectError(code, resp, self.log.str())) |
|---|
| 1028 |
|
|---|
| 1029 |
def smtpTransferFailed(self, code, resp): |
|---|
| 1030 |
if code < 0: |
|---|
| 1031 |
self.sendError(SMTPProtocolError(code, resp, self.log.str())) |
|---|
| 1032 |
else: |
|---|
| 1033 |
self.smtpState_msgSent(code, resp) |
|---|
| 1034 |
|
|---|
| 1035 |
def smtpState_helo(self, code, resp): |
|---|
| 1036 |
self.sendLine('HELO ' + self.identity) |
|---|
| 1037 |
self._expected = SUCCESS |
|---|
| 1038 |
self._okresponse = self.smtpState_from |
|---|
| 1039 |
|
|---|
| 1040 |
def smtpState_from(self, code, resp): |
|---|
| 1041 |
self._from = self.getMailFrom() |
|---|
| 1042 |
self._failresponse = self.smtpTransferFailed |
|---|
| 1043 |
if self._from is not None: |
|---|
| 1044 |
self.sendLine('MAIL FROM:%s' % quoteaddr(self._from)) |
|---|
| 1045 |
self._expected = [250] |
|---|
| 1046 |
self._okresponse = self.smtpState_to |
|---|
| 1047 |
else: |
|---|
| 1048 |
|
|---|
| 1049 |
self._disconnectFromServer() |
|---|
| 1050 |
|
|---|
| 1051 |
def smtpState_disconnect(self, code, resp): |
|---|
| 1052 |
self.transport.loseConnection() |
|---|
| 1053 |
|
|---|
| 1054 |
def smtpState_to(self, code, resp): |
|---|
| 1055 |
self.toAddresses = iter(self.getMailTo()) |
|---|
| 1056 |
self.toAddressesResult = [] |
|---|
| 1057 |
self.successAddresses = [] |
|---|
| 1058 |
self._okresponse = self.smtpState_toOrData |
|---|
| 1059 |
self._expected = xrange(0,1000) |
|---|
| 1060 |
self.lastAddress = None |
|---|
| 1061 |
return self.smtpState_toOrData(0, '') |
|---|
| 1062 |
|
|---|
| 1063 |
def smtpState_toOrData(self, code, resp): |
|---|
| 1064 |
if self.lastAddress is not None: |
|---|
| 1065 |
self.toAddressesResult.append((self.lastAddress, code, resp)) |
|---|
| 1066 |
if code in SUCCESS: |
|---|
| 1067 |
self.successAddresses.append(self.lastAddress) |
|---|
| 1068 |
try: |
|---|
| 1069 |
self.lastAddress = self.toAddresses.next() |
|---|
| 1070 |
except StopIteration: |
|---|
| 1071 |
if self.successAddresses: |
|---|
| 1072 |
self.sendLine('DATA') |
|---|
| 1073 |
self._expected = [ 354 ] |
|---|
| 1074 |
self._okresponse = self.smtpState_data |
|---|
| 1075 |
else: |
|---|
| 1076 |
return self.smtpState_msgSent(code,'No recipients accepted') |
|---|
| 1077 |
else: |
|---|
| 1078 |
self.sendLine('RCPT TO:%s' % quoteaddr(self.lastAddress)) |
|---|
| 1079 |
|
|---|
| 1080 |
def smtpState_data(self, code, resp): |
|---|
| 1081 |
s = basic.FileSender() |
|---|
| 1082 |
s.beginFileTransfer( |
|---|
| 1083 |
self.getMailData(), self.transport, self.transformChunk |
|---|
| 1084 |
).addCallback(self.finishedFileTransfer) |
|---|
| 1085 |
self._expected = SUCCESS |
|---|
| 1086 |
self._okresponse = self.smtpState_msgSent |
|---|
| 1087 |
|
|---|
| 1088 |
def smtpState_msgSent(self, code, resp): |
|---|
| 1089 |
if self._from is not None: |
|---|
| 1090 |
self.sentMail(code, resp, len(self.successAddresses), |
|---|
| 1091 |
self.toAddressesResult, self.log) |
|---|
| 1092 |
|
|---|
| 1093 |
self.toAddressesResult = [] |
|---|
| 1094 |
self._from = None |
|---|
| 1095 |
self.sendLine('RSET') |
|---|
| 1096 |
self._expected = SUCCESS |
|---|
| 1097 |
self._okresponse = self.smtpState_from |
|---|
| 1098 |
|
|---|
| 1099 |
|
|---|
| 1100 |
|
|---|
| 1101 |
|
|---|
| 1102 |
def transformChunk(self, chunk): |
|---|
| 1103 |
return chunk.replace('\n', '\r\n').replace('\r\n.', '\r\n..') |
|---|
| 1104 |
|
|---|
| 1105 |
def finishedFileTransfer(self, lastsent): |
|---|
| 1106 |
if lastsent != '\n': |
|---|
| 1107 |
line = '\r\n.' |
|---|
| 1108 |
else: |
|---|
| 1109 |
line = '.' |
|---|
| 1110 |
self.sendLine(line) |
|---|
| 1111 |
|
|---|
| 1112 |
|
|---|
| 1113 |
|
|---|
| 1114 |
def getMailFrom(self): |
|---|
| 1115 |
"""Return the email address the mail is from.""" |
|---|
| 1116 |
raise NotImplementedError |
|---|
| 1117 |
|
|---|
| 1118 |
def getMailTo(self): |
|---|
| 1119 |
"""Return a list of emails to send to.""" |
|---|
| 1120 |
raise NotImplementedError |
|---|
| 1121 |
|
|---|
| 1122 |
def getMailData(self): |
|---|
| 1123 |
"""Return file-like object containing data of message to be sent. |
|---|
| 1124 |
|
|---|
| 1125 |
Lines in the file should be delimited by '\\n'. |
|---|
| 1126 |
""" |
|---|
| 1127 |
raise NotImplementedError |
|---|
| 1128 |
|
|---|
| 1129 |
def sendError(self, exc): |
|---|
| 1130 |
"""If an error occurs before a mail message is sent sendError will be called. |
|---|
| 1131 |
This base class method sends a QUIT if the error is non-fatal |
|---|
| 1132 |
and disconnects the connection. |
|---|
| 1133 |
|
|---|
| 1134 |
@param exc: The SMTPClientError (or child class) raised |
|---|
| 1135 |
@type exc: C{SMTPClientError} |
|---|
| 1136 |
""" |
|---|
| 1137 |
assert isinstance(exc, SMTPClientError) |
|---|
| 1138 |
|
|---|
| 1139 |
if exc.isFatal: |
|---|
| 1140 |
|
|---|
| 1141 |
|
|---|
| 1142 |
self.smtpState_disconnect(-1, None) |
|---|
| 1143 |
else: |
|---|
| 1144 |
self._disconnectFromServer() |
|---|
| 1145 |
|
|---|
| 1146 |
def sentMail(self, code, resp, numOk, addresses, log): |
|---|
| 1147 |
"""Called when an attempt to send an email is completed. |
|---|
| 1148 |
|
|---|
| 1149 |
If some addresses were accepted, code and resp are the response |
|---|
| 1150 |
to the DATA command. If no addresses were accepted, code is -1 |
|---|
| 1151 |
and resp is an informative message. |
|---|
| 1152 |
|
|---|
| 1153 |
@param code: the code returned by the SMTP Server |
|---|
| 1154 |
@param resp: The string response returned from the SMTP Server |
|---|
| 1155 |
@param numOK: the number of addresses accepted by the remote host. |
|---|
| 1156 |
@param addresses: is a list of tuples (address, code, resp) listing |
|---|
| 1157 |
the response to each RCPT command. |
|---|
| 1158 |
@param log: is the SMTP session log |
|---|
| 1159 |
""" |
|---|
| 1160 |
raise NotImplementedError |
|---|
| 1161 |
|
|---|
| 1162 |
def _disconnectFromServer(self): |
|---|
| 1163 |
self._expected = xrange(0, 1000) |
|---|
| 1164 |
self._okresponse = self.smtpState_disconnect |
|---|
| 1165 |
self.sendLine('QUIT') |
|---|
| 1166 |
|
|---|
| 1167 |
class ESMTPClient(SMTPClient): |
|---|
| 1168 |
|
|---|
| 1169 |
heloFallback = True |
|---|
| 1170 |
|
|---|
| 1171 |
|
|---|
| 1172 |
requireAuthentication = False |
|---|
| 1173 |
|
|---|
| 1174 |
|
|---|
| 1175 |
requireTransportSecurity = False |
|---|
| 1176 |
|
|---|
| 1177 |
|
|---|
| 1178 |
tlsMode = False |
|---|
| 1179 |
|
|---|
| 1180 |
|
|---|
| 1181 |
context = None |
|---|
| 1182 |
|
|---|
| 1183 |
def __init__(self, secret, contextFactory=None, *args, **kw): |
|---|
| 1184 |
SMTPClient.__init__(self, *args, **kw) |
|---|
| 1185 |
self.authenticators = [] |
|---|
| 1186 |
self.secret = secret |
|---|
| 1187 |
self.context = contextFactory |
|---|
| 1188 |
self.tlsMode = False |
|---|
| 1189 |
|
|---|
| 1190 |
def esmtpEHLORequired(self, code=-1, resp=None): |
|---|
| 1191 |
self.sendError(EHLORequiredError(502, "Server does not support ESMTP Authentication", self.log.str())) |
|---|
| 1192 |
|
|---|
| 1193 |
def esmtpAUTHRequired(self, code=-1, resp=None): |
|---|
| 1194 |
tmp = [] |
|---|
| 1195 |
|
|---|
| 1196 |
for a in self.authenticators: |
|---|
| 1197 |
tmp.append(a.getName().upper()) |
|---|
| 1198 |
|
|---|
| 1199 |
auth = "[%s]" % ', '.join(tmp) |
|---|
| 1200 |
|
|---|
| 1201 |
self.sendError(AUTHRequiredError(502, "Server does not support Client Authentication schemes %s" % auth, |
|---|
| 1202 |
self.log.str())) |
|---|
| 1203 |
|
|---|
| 1204 |
def esmtpTLSRequired(self, code=-1, resp=None): |
|---|
| 1205 |
self.sendError(TLSRequiredError(502, "Server does not support secure communication via TLS / SSL", |
|---|
| 1206 |
self.log.str())) |
|---|
| 1207 |
|
|---|
| 1208 |
def esmtpTLSFailed(self, code=-1, resp=None): |
|---|
| 1209 |
self.sendError(TLSError(code, "Could not complete the SSL/TLS handshake", self.log.str())) |
|---|
| 1210 |
|
|---|
| 1211 |
def esmtpAUTHDeclined(self, code=-1, resp=None): |
|---|
| 1212 |
self.sendError(AUTHDeclinedError(code, resp, self.log.str())) |
|---|
| 1213 |
|
|---|
| 1214 |
def esmtpAUTHMalformedChallenge(self, code=-1, resp=None): |
|---|
| 1215 |
str = "Login failed because the SMTP Server returned a malformed Authentication Challenge" |
|---|
| 1216 |
self.sendError(AuthenticationError(501, str, self.log.str())) |
|---|
| 1217 |
|
|---|
| 1218 |
def esmtpAUTHServerError(self, code=-1, resp=None): |
|---|
| 1219 |
self.sendError(AuthenticationError(code, resp, self.log.str())) |
|---|
| 1220 |
|
|---|
| 1221 |
def registerAuthenticator(self, auth): |
|---|
| 1222 |
"""Registers an Authenticator with the ESMTPClient. The ESMTPClient |
|---|
| 1223 |
will attempt to login to the SMTP Server in the order the |
|---|
| 1224 |
Authenticators are registered. The most secure Authentication |
|---|
| 1225 |
mechanism should be registered first. |
|---|
| 1226 |
|
|---|
| 1227 |
@param auth: The Authentication mechanism to register |
|---|
| 1228 |
@type auth: class implementing C{IClientAuthentication} |
|---|
| 1229 |
""" |
|---|
| 1230 |
|
|---|
| 1231 |
self.authenticators.append(auth) |
|---|
| 1232 |
|
|---|
| 1233 |
def connectionMade(self): |
|---|
| 1234 |
SMTPClient.connectionMade(self) |
|---|
| 1235 |
self._okresponse = self.esmtpState_ehlo |
|---|
| 1236 |
|
|---|
| 1237 |
def esmtpState_ehlo(self, code, resp): |
|---|
| 1238 |
self._expected = SUCCESS |
|---|
| 1239 |
|
|---|
| 1240 |
self._okresponse = self.esmtpState_serverConfig |
|---|
| 1241 |
self._failresponse = self.esmtpEHLORequired |
|---|
| 1242 |
|
|---|
| 1243 |
if self.heloFallback: |
|---|
| 1244 |
self._failresponse = self.smtpState_helo |
|---|
| 1245 |
|
|---|
| 1246 |
self.sendLine('EHLO ' + self.identity) |
|---|
| 1247 |
|
|---|
| 1248 |
def esmtpState_serverConfig(self, code, resp): |
|---|
| 1249 |
items = {} |
|---|
| 1250 |
for line in resp.splitlines(): |
|---|
| 1251 |
e = line.split(None, 1) |
|---|
| 1252 |
if len(e) > 1: |
|---|
| 1253 |
items[e[0]] = e[1] |
|---|
| 1254 |
else: |
|---|
| 1255 |
items[e[0]] = None |
|---|
| 1256 |
|
|---|
| 1257 |
if self.tlsMode: |
|---|
| 1258 |
self.authenticate(code, resp, items) |
|---|
| 1259 |
else: |
|---|
| 1260 |
self.tryTLS(code, resp, items) |
|---|
| 1261 |
|
|---|
| 1262 |
def tryTLS(self, code, resp, items): |
|---|
| 1263 |
if self.context and 'STARTTLS' in items: |
|---|
| 1264 |
self._expected = [220] |
|---|
| 1265 |
self._okresponse = self.esmtpState_starttls |
|---|
| 1266 |
self._failresponse = self.esmtpTLSFailed |
|---|
| 1267 |
self.sendLine('STARTTLS') |
|---|
| 1268 |
elif self.requireTransportSecurity: |
|---|
| 1269 |
self.tlsMode = False |
|---|
| 1270 |
self.esmtpTLSRequired() |
|---|
| 1271 |
else: |
|---|
| 1272 |
self.tlsMode = False |
|---|
| 1273 |
self.authenticate(code, resp, items) |
|---|
| 1274 |
|
|---|
| 1275 |
def esmtpState_starttls(self, code, resp): |
|---|
| 1276 |
try: |
|---|
| 1277 |
self.transport.startTLS(self.context) |
|---|
| 1278 |
self.tlsMode = True |
|---|
| 1279 |
except: |
|---|
| 1280 |
log.err() |
|---|
| 1281 |
self.esmtpTLSFailed(451) |
|---|
| 1282 |
|
|---|
| 1283 |
|
|---|
| 1284 |
|
|---|
| 1285 |
self.esmtpState_ehlo(code, resp) |
|---|
| 1286 |
|
|---|
| 1287 |
def authenticate(self, code, resp, items): |
|---|
| 1288 |
if self.secret and items.get('AUTH'): |
|---|
| 1289 |
schemes = items['AUTH'].split() |
|---|
| 1290 |
tmpSchemes = {} |
|---|
| 1291 |
|
|---|
| 1292 |
|
|---|
| 1293 |
for s in schemes: |
|---|
| 1294 |
tmpSchemes[s.upper()] = 1 |
|---|
| 1295 |
|
|---|
| 1296 |
for a in self.authenticators: |
|---|
| 1297 |
auth = a.getName().upper() |
|---|
| 1298 |
|
|---|
| 1299 |
if auth in tmpSchemes: |
|---|
| 1300 |
self._authinfo = a |
|---|
| 1301 |
|
|---|
| 1302 |
|
|---|
| 1303 |
if auth == "PLAIN": |
|---|
| 1304 |
self._okresponse = self.smtpState_from |
|---|
| 1305 |
self._failresponse = self._esmtpState_plainAuth |
|---|
| 1306 |
self._expected = [235] |
|---|
| 1307 |
challenge = encode_base64(self._authinfo.challengeResponse(self.secret, 1), eol="") |
|---|
| 1308 |
self.sendLine('AUTH ' + auth + ' ' + challenge) |
|---|
| 1309 |
else: |
|---|
| 1310 |
self._expected = [334] |
|---|
| 1311 |
self._okresponse = self.esmtpState_challenge |
|---|
| 1312 |
|
|---|
| 1313 |
|
|---|
| 1314 |
|
|---|
| 1315 |
self._failresponse = self.esmtpAUTHServerError |
|---|
| 1316 |
self.sendLine('AUTH ' + auth) |
|---|
| 1317 |
return |
|---|
| 1318 |
|
|---|
| 1319 |
if self.requireAuthentication: |
|---|
| 1320 |
self.esmtpAUTHRequired() |
|---|
| 1321 |
else: |
|---|
| 1322 |
self.smtpState_from(code, resp) |
|---|
| 1323 |
|
|---|
| 1324 |
def _esmtpState_plainAuth(self, code, resp): |
|---|
| 1325 |
self._okresponse = self.smtpState_from |
|---|
| 1326 |
self._failresponse = self.esmtpAUTHDeclined |
|---|
| 1327 |
self._expected = [235] |
|---|
| 1328 |
challenge = encode_base64(self._authinfo.challengeResponse(self.secret, 2), eol="") |
|---|
| 1329 |
self.sendLine('AUTH PLAIN ' + challenge) |
|---|
| 1330 |
|
|---|
| 1331 |
def esmtpState_challenge(self, code, resp): |
|---|
| 1332 |
auth = self._authinfo |
|---|
| 1333 |
del self._authinfo |
|---|
| 1334 |
self._authResponse(auth, resp) |
|---|
| 1335 |
|
|---|
| 1336 |
def _authResponse(self, auth, challenge): |
|---|
| 1337 |
self._failresponse = self.esmtpAUTHDeclined |
|---|
| 1338 |
|
|---|
| 1339 |
try: |
|---|
| 1340 |
challenge = base64.decodestring(challenge) |
|---|
| 1341 |
except binascii.Error, e: |
|---|
| 1342 |
|
|---|
| 1343 |
self.sendLine('*') |
|---|
| 1344 |
self._okresponse = self.esmtpAUTHMalformedChallenge |
|---|
| 1345 |
self._failresponse = self.esmtpAUTHMalformedChallenge |
|---|
| 1346 |
else: |
|---|
| 1347 |
resp = auth.challengeResponse(self.secret, challenge) |
|---|
| 1348 |
self._expected = [235] |
|---|
| 1349 |
self._okresponse = self.smtpState_from |
|---|
| 1350 |
self.sendLine(encode_base64(resp, eol="")) |
|---|
| 1351 |
|
|---|
| 1352 |
if auth.getName() == "LOGIN" and challenge == "Username:": |
|---|
| 1353 |
self._expected = [334] |
|---|
| 1354 |
self._authinfo = auth |
|---|
| 1355 |
self._okresponse = self.esmtpState_challenge |
|---|
| 1356 |
|
|---|
| 1357 |
|
|---|
| 1358 |
class ESMTP(SMTP): |
|---|
| 1359 |
|
|---|
| 1360 |
ctx = None |
|---|
| 1361 |
canStartTLS = False |
|---|
| 1362 |
startedTLS = False |
|---|
| 1363 |
|
|---|
| 1364 |
authenticated = False |
|---|
| 1365 |
|
|---|
| 1366 |
def __init__(self, chal = None, contextFactory = None): |
|---|
| 1367 |
SMTP.__init__(self) |
|---|
| 1368 |
if chal is None: |
|---|
| 1369 |
chal = {} |
|---|
| 1370 |
self.challengers = chal |
|---|
| 1371 |
self.authenticated = False |
|---|
| 1372 |
self.ctx = contextFactory |
|---|
| 1373 |
|
|---|
| 1374 |
def connectionMade(self): |
|---|
| 1375 |
SMTP.connectionMade(self) |
|---|
| 1376 |
self.canStartTLS = ITLSTransport.providedBy(self.transport) |
|---|
| 1377 |
self.canStartTLS = self.canStartTLS and (self.ctx is not None) |
|---|
| 1378 |
|
|---|
| 1379 |
def extensions(self): |
|---|
| 1380 |
ext = {'AUTH': self.challengers.keys()} |
|---|
| 1381 |
if self.canStartTLS and not self.startedTLS: |
|---|
| 1382 |
ext['STARTTLS'] = None |
|---|
| 1383 |
return ext |
|---|
| 1384 |
|
|---|
| 1385 |
def lookupMethod(self, command): |
|---|
| 1386 |
m = SMTP.lookupMethod(self, command) |
|---|
| 1387 |
if m is None: |
|---|
| 1388 |
m = getattr(self, 'ext_' + command.upper(), None) |
|---|
| 1389 |
return m |
|---|
| 1390 |
|
|---|
| 1391 |
def listExtensions(self): |
|---|
| 1392 |
r = [] |
|---|
| 1393 |
for (c, v) in self.extensions().iteritems(): |
|---|
| 1394 |
if v is not None: |
|---|
| 1395 |
if v: |
|---|
| 1396 |
|
|---|
| 1397 |
r.append('%s %s' % (c, ' '.join(v))) |
|---|
| 1398 |
else: |
|---|
| 1399 |
r.append(c) |
|---|
| 1400 |
return '\n'.join(r) |
|---|
| 1401 |
|
|---|
| 1402 |
def do_EHLO(self, rest): |
|---|
| 1403 |
peer = self.transport.getPeer().host |
|---|
| 1404 |
self._helo = (rest, peer) |
|---|
| 1405 |
self._from = None |
|---|
| 1406 |
self._to = [] |
|---|
| 1407 |
self.sendCode( |
|---|
| 1408 |
250, |
|---|
| 1409 |
'%s Hello %s, nice to meet you\n%s' % ( |
|---|
| 1410 |
self.host, peer, |
|---|
| 1411 |
self.listExtensions(), |
|---|
| 1412 |
) |
|---|
| 1413 |
) |
|---|
| 1414 |
|
|---|
| 1415 |
def ext_STARTTLS(self, rest): |
|---|
| 1416 |
if self.startedTLS: |
|---|
| 1417 |
self.sendCode(503, 'TLS already negotiated') |
|---|
| 1418 |
elif self.ctx and self.canStartTLS: |
|---|
| 1419 |
self.sendCode(220, 'Begin TLS negotiation now') |
|---|
| 1420 |
self.transport.startTLS(self.ctx) |
|---|
| 1421 |
self.startedTLS = True |
|---|
| 1422 |
else: |
|---|
| 1423 |
self.sendCode(454, 'TLS not available') |
|---|
| 1424 |
|
|---|
| 1425 |
def ext_AUTH(self, rest): |
|---|
| 1426 |
if self.authenticated: |
|---|
| 1427 |
self.sendCode(503, 'Already authenticated') |
|---|
| 1428 |
return |
|---|
| 1429 |
parts = rest.split(None, 1) |
|---|
| 1430 |
chal = self.challengers.get(parts[0].upper(), lambda: None)() |
|---|
| 1431 |
if not chal: |
|---|
| 1432 |
self.sendCode(504, 'Unrecognized authentication type') |
|---|
| 1433 |
return |
|---|
| 1434 |
self.authenticate(chal) |
|---|
| 1435 |
|
|---|
| 1436 |
def authenticate(self, challenger): |
|---|
| 1437 |
if self.portal: |
|---|
| 1438 |
challenge = challenger.getChallenge() |
|---|
| 1439 |
coded = base64.encodestring(challenge)[:-1] |
|---|
| 1440 |
self.sendCode(334, coded) |
|---|
| 1441 |
self.mode = AUTH |
|---|
| 1442 |
self.challenger = challenger |
|---|
| 1443 |
else: |
|---|
| 1444 |
self.sendCode(454, 'Temporary authentication failure') |
|---|
| 1445 |
|
|---|
| 1446 |
def state_AUTH(self, rest): |
|---|
| 1447 |
self.mode = COMMAND |
|---|
| 1448 |
|
|---|
| 1449 |
if rest == '*': |
|---|
| 1450 |
self.sendCode(501, 'Authentication aborted') |
|---|
| 1451 |
self.challenger.abort() |
|---|
| 1452 |
self.challenger = None |
|---|
| 1453 |
return |
|---|
| 1454 |
|
|---|
| 1455 |
try: |
|---|
| 1456 |
uncoded = base64.decodestring(rest) |
|---|
| 1457 |
except binascii.Error, e: |
|---|
| 1458 |
self._ebAuthenticated(failure.Failure(e)) |
|---|
| 1459 |
else: |
|---|
| 1460 |
self.challenger.setResponse(uncoded) |
|---|
| 1461 |
if self.challenger.moreChallenges(): |
|---|
| 1462 |
self.authenticate(self.challenger) |
|---|
| 1463 |
else: |
|---|
| 1464 |
self.portal.login(self.challenger, None, |
|---|
| 1465 |
IMessageDeliveryFactory, IMessageDelivery |
|---|
| 1466 |
).addCallback(self._cbAuthenticated |
|---|
| 1467 |
).addCallback(lambda _: self.sendCode(235, 'Authentication successful.') |
|---|
| 1468 |
).addErrback(self._ebAuthenticated |
|---|
| 1469 |
) |
|---|
| 1470 |
|
|---|
| 1471 |
class SenderMixin: |
|---|
| 1472 |
"""Utility class for sending emails easily. |
|---|
| 1473 |
|
|---|
| 1474 |
Use with SMTPSenderFactory or ESMTPSenderFactory. |
|---|
| 1475 |
""" |
|---|
| 1476 |
done = 0 |
|---|
| 1477 |
|
|---|
| 1478 |
def getMailFrom(self): |
|---|
| 1479 |
if not self.done: |
|---|
| 1480 |
self.done = 1 |
|---|
| 1481 |
return str(self.factory.fromEmail) |
|---|
| 1482 |
else: |
|---|
| 1483 |
return None |
|---|
| 1484 |
|
|---|
| 1485 |
def getMailTo(self): |
|---|
| 1486 |
return self.factory.toEmail |
|---|
| 1487 |
|
|---|
| 1488 |
def getMailData(self): |
|---|
| 1489 |
return self.factory.file |
|---|
| 1490 |
|
|---|
| 1491 |
def sendError(self, exc): |
|---|
| 1492 |
|
|---|
| 1493 |
SMTPClient.sendError(self, exc) |
|---|
| 1494 |
|
|---|
| 1495 |
|
|---|
| 1496 |
|
|---|
| 1497 |
|
|---|
| 1498 |
|
|---|
| 1499 |
|
|---|
| 1500 |
if (self.factory.retries >= 0 or |
|---|
| 1501 |
(not exc.retry and not (exc.code >= 400 and exc.code < 500))): |
|---|
| 1502 |
self.factory.sendFinished = 1 |
|---|
| 1503 |
self.factory.result.errback(exc) |
|---|
| 1504 |
|
|---|
| 1505 |
def sentMail(self, code, resp, numOk, addresses, log): |
|---|
| 1506 |
|
|---|
| 1507 |
self.factory.sendFinished = 1 |
|---|
| 1508 |
if code not in SUCCESS: |
|---|
| 1509 |
errlog = [] |
|---|
| 1510 |
for addr, acode, aresp in addresses: |
|---|
| 1511 |
if code not in SUCCESS: |
|---|
| 1512 |
errlog.append("%s: %03d %s" % (addr, acode, aresp)) |
|---|
| 1513 |
|
|---|
| 1514 |
errlog.append(log.str()) |
|---|
| 1515 |
|
|---|
| 1516 |
exc = SMTPDeliveryError(code, resp, '\n'.join(errlog), addresses) |
|---|
| 1517 |
self.factory.result.errback(exc) |
|---|
| 1518 |
else: |
|---|
| 1519 |
self.factory.result.callback((numOk, addresses)) |
|---|
| 1520 |
|
|---|
| 1521 |
|
|---|
| 1522 |
class SMTPSender(SenderMixin, SMTPClient): |
|---|
| 1523 |
pass |
|---|
| 1524 |
|
|---|
| 1525 |
|
|---|
| 1526 |
class SMTPSenderFactory(protocol.ClientFactory): |
|---|
| 1527 |
""" |
|---|
| 1528 |
Utility factory for sending emails easily. |
|---|
| 1529 |
""" |
|---|
| 1530 |
|
|---|
| 1531 |
domain = DNSNAME |
|---|
| 1532 |
protocol = SMTPSender |
|---|
| 1533 |
|
|---|
| 1534 |
def __init__(self, fromEmail, toEmail, file, deferred, retries=5, |
|---|
| 1535 |
timeout=None): |
|---|
| 1536 |
""" |
|---|
| 1537 |
@param fromEmail: The RFC 2821 address from which to send this |
|---|
| 1538 |
message. |
|---|
| 1539 |
|
|---|
| 1540 |
@param toEmail: A sequence of RFC 2821 addresses to which to |
|---|
| 1541 |
send this message. |
|---|
| 1542 |
|
|---|
| 1543 |
@param file: A file-like object containing the message to send. |
|---|
| 1544 |
|
|---|
| 1545 |
@param deferred: A Deferred to callback or errback when sending |
|---|
| 1546 |
of this message completes. |
|---|
| 1547 |
|
|---|
| 1548 |
@param retries: The number of times to retry delivery of this |
|---|
| 1549 |
message. |
|---|
| 1550 |
|
|---|
| 1551 |
@param timeout: Period, in seconds, for which to wait for |
|---|
| 1552 |
server responses, or None to wait forever. |
|---|
| 1553 |
""" |
|---|
| 1554 |
assert isinstance(retries, (int, long)) |
|---|
| 1555 |
|
|---|
| 1556 |
if isinstance(toEmail, types.StringTypes): |
|---|
| 1557 |
toEmail = [toEmail] |
|---|
| 1558 |
self.fromEmail = Address(fromEmail) |
|---|
| 1559 |
self.nEmails = len(toEmail) |
|---|
| 1560 |
self.toEmail = iter(toEmail) |
|---|
| 1561 |
self.file = file |
|---|
| 1562 |
self.result = deferred |
|---|
| 1563 |
self.result.addBoth(self._removeDeferred) |
|---|
| 1564 |
self.sendFinished = 0 |
|---|
| 1565 |
|
|---|
| 1566 |
self.retries = -retries |
|---|
| 1567 |
self.timeout = timeout |
|---|
| 1568 |
|
|---|
| 1569 |
def _removeDeferred(self, argh): |
|---|
| 1570 |
del self.result |
|---|
| 1571 |
return argh |
|---|
| 1572 |
|
|---|
| 1573 |
def clientConnectionFailed(self, connector, err): |
|---|
| 1574 |
self._processConnectionError(connector, err) |
|---|
| 1575 |
|
|---|
| 1576 |
def clientConnectionLost(self, connector, err): |
|---|
| 1577 |
self._processConnectionError(connector, err) |
|---|
| 1578 |
|
|---|
| 1579 |
def _processConnectionError(self, connector, err): |
|---|
| 1580 |
if self.retries < self.sendFinished <= 0: |
|---|
| 1581 |
log.msg("SMTP Client retrying server. Retry: %s" % -self.retries) |
|---|
| 1582 |
|
|---|
| 1583 |
connector.connect() |
|---|
| 1584 |
self.retries += 1 |
|---|
| 1585 |
elif self.sendFinished <= 0: |
|---|
| 1586 |
|
|---|
| 1587 |
|
|---|
| 1588 |
if err.check(error.ConnectionDone): |
|---|
| 1589 |
err.value = SMTPConnectError(-1, "Unable to connect to server.") |
|---|
| 1590 |
self.result.errback(err.value) |
|---|
| 1591 |
|
|---|
| 1592 |
def buildProtocol(self, addr): |
|---|
| 1593 |
p = self.protocol(self.domain, self.nEmails*2+2) |
|---|
| 1594 |
p.factory = self |
|---|
| 1595 |
p.timeout = self.timeout |
|---|
| 1596 |
return p |
|---|
| 1597 |
|
|---|
| 1598 |
|
|---|
| 1599 |
class IClientAuthentication(components.Interface): |
|---|
| 1600 |
def getName(self): |
|---|
| 1601 |
"""Return an identifier associated with this authentication scheme. |
|---|
| 1602 |
|
|---|
| 1603 |
@rtype: C{str} |
|---|
| 1604 |
""" |
|---|
| 1605 |
|
|---|
| 1606 |
def challengeResponse(self, secret, challenge): |
|---|
| 1607 |
"""Generate a challenge response string""" |
|---|
| 1608 |
|
|---|
| 1609 |
|
|---|
| 1610 |
class CramMD5ClientAuthenticator: |
|---|
| 1611 |
implements(IClientAuthentication) |
|---|
| 1612 |
|
|---|
| 1613 |
def __init__(self, user): |
|---|
| 1614 |
self.user = user |
|---|
| 1615 |
|
|---|
| 1616 |
def getName(self): |
|---|
| 1617 |
return "CRAM-MD5" |
|---|
| 1618 |
|
|---|
| 1619 |
def challengeResponse(self, secret, chal): |
|---|
| 1620 |
response = hmac.HMAC(secret, chal).hexdigest() |
|---|
| 1621 |
return '%s %s' % (self.user, response) |
|---|
| 1622 |
|
|---|
| 1623 |
components.backwardsCompatImplements(CramMD5ClientAuthenticator) |
|---|
| 1624 |
|
|---|
| 1625 |
|
|---|
| 1626 |
class LOGINAuthenticator: |
|---|
| 1627 |
implements(IClientAuthentication) |
|---|
| 1628 |
|
|---|
| 1629 |
def __init__(self, user): |
|---|
| 1630 |
self.user = user |
|---|
| 1631 |
|
|---|
| 1632 |
def getName(self): |
|---|
| 1633 |
return "LOGIN" |
|---|
| 1634 |
|
|---|
| 1635 |
def challengeResponse(self, secret, chal): |
|---|
| 1636 |
if chal== "Username:": |
|---|
| 1637 |
return self.user |
|---|
| 1638 |
elif chal == 'Password:': |
|---|
| 1639 |
return secret |
|---|
| 1640 |
|
|---|
| 1641 |
components.backwardsCompatImplements(LOGINAuthenticator) |
|---|
| 1642 |
|
|---|
| 1643 |
class PLAINAuthenticator: |
|---|
| 1644 |
implements(IClientAuthentication) |
|---|
| 1645 |
|
|---|
| 1646 |
def __init__(self, user): |
|---|
| 1647 |
self.user = user |
|---|
| 1648 |
|
|---|
| 1649 |
def getName(self): |
|---|
| 1650 |
return "PLAIN" |
|---|
| 1651 |
|
|---|
| 1652 |
def challengeResponse(self, secret, chal=1): |
|---|
| 1653 |
if chal == 1: |
|---|
| 1654 |
return "%s\0%s\0%s" % (self.user, self.user, secret) |
|---|
| 1655 |
else: |
|---|
| 1656 |
return "%s\0%s" % (self.user, secret) |
|---|
| 1657 |
|
|---|
| 1658 |
components.backwardsCompatImplements(PLAINAuthenticator) |
|---|
| 1659 |
|
|---|
| 1660 |
|
|---|
| 1661 |
class ESMTPSender(SenderMixin, ESMTPClient): |
|---|
| 1662 |
|
|---|
| 1663 |
requireAuthentication = True |
|---|
| 1664 |
requireTransportSecurity = True |
|---|
| 1665 |
|
|---|
| 1666 |
def __init__(self, username, secret, contextFactory=None, *args, **kw): |
|---|
| 1667 |
self.heloFallback = 0 |
|---|
| 1668 |
self.username = username |
|---|
| 1669 |
|
|---|
| 1670 |
if contextFactory is None: |
|---|
| 1671 |
contextFactory = self._getContextFactory() |
|---|
| 1672 |
|
|---|
| 1673 |
ESMTPClient.__init__(self, secret, contextFactory, *args, **kw) |
|---|
| 1674 |
|
|---|
| 1675 |
self._registerAuthenticators() |
|---|
| 1676 |
|
|---|
| 1677 |
def _registerAuthenticators(self): |
|---|
| 1678 |
|
|---|
| 1679 |
self.registerAuthenticator(CramMD5ClientAuthenticator(self.username)) |
|---|
| 1680 |
self.registerAuthenticator(LOGINAuthenticator(self.username)) |
|---|
| 1681 |
self.registerAuthenticator(PLAINAuthenticator(self.username)) |
|---|
| 1682 |
|
|---|
| 1683 |
def _getContextFactory(self): |
|---|
| 1684 |
if self.context is not None: |
|---|
| 1685 |
return self.context |
|---|
| 1686 |
try: |
|---|
| 1687 |
from twisted.internet import ssl |
|---|
| 1688 |
except ImportError: |
|---|
| 1689 |
return None |
|---|
| 1690 |
else: |
|---|
| 1691 |
try: |
|---|
| 1692 |
context = ssl.ClientContextFactory() |
|---|
| 1693 |
context.method = ssl.SSL.TLSv1_METHOD |
|---|
| 1694 |
return context |
|---|
| 1695 |
except AttributeError: |
|---|
| 1696 |
return None |
|---|
| 1697 |
|
|---|
| 1698 |
|
|---|
| 1699 |
class ESMTPSenderFactory(SMTPSenderFactory): |
|---|
| 1700 |
""" |
|---|
| 1701 |
Utility factory for sending emails easily. |
|---|
| 1702 |
""" |
|---|
| 1703 |
|
|---|
| 1704 |
protocol = ESMTPSender |
|---|
| 1705 |
|
|---|
| 1706 |
def __init__(self, username, password, fromEmail, toEmail, file, |
|---|
| 1707 |
deferred, retries=5, timeout=None, |
|---|
| 1708 |
contextFactory=None, heloFallback=False, |
|---|
| 1709 |
requireAuthentication=True, |
|---|
| 1710 |
requireTransportSecurity=True): |
|---|
| 1711 |
|
|---|
| 1712 |
SMTPSenderFactory.__init__(self, fromEmail, toEmail, file, deferred, retries, timeout) |
|---|
| 1713 |
self.username = username |
|---|
| 1714 |
self.password = password |
|---|
| 1715 |
self._contextFactory = contextFactory |
|---|
| 1716 |
self._heloFallback = heloFallback |
|---|
| 1717 |
self._requireAuthentication = requireAuthentication |
|---|
| 1718 |
self._requireTransportSecurity = requireTransportSecurity |
|---|
| 1719 |
|
|---|
| 1720 |
def buildProtocol(self, addr): |
|---|
| 1721 |
p = self.protocol(self.username, self.password, self._contextFactory, self.domain, self.nEmails*2+2) |
|---|
| 1722 |
p.heloFallback = self._heloFallback |
|---|
| 1723 |
p.requireAuthentication = self._requireAuthentication |
|---|
| 1724 |
p.requireTransportSecurity = self._requireTransportSecurity |
|---|
| 1725 |
p.factory = self |
|---|
| 1726 |
p.timeout = self.timeout |
|---|
| 1727 |
return p |
|---|
| 1728 |
|
|---|
| 1729 |
def sendmail(smtphost, from_addr, to_addrs, msg, senderDomainName=None, port=25): |
|---|
| 1730 |
"""Send an email |
|---|
| 1731 |
|
|---|
| 1732 |
This interface is intended to be a direct replacement for |
|---|
| 1733 |
smtplib.SMTP.sendmail() (with the obvious change that |
|---|
| 1734 |
you specify the smtphost as well). Also, ESMTP options |
|---|
| 1735 |
are not accepted, as we don't do ESMTP yet. I reserve the |
|---|
| 1736 |
right to implement the ESMTP options differently. |
|---|
| 1737 |
|
|---|
| 1738 |
@param smtphost: The host the message should be sent to |
|---|
| 1739 |
@param from_addr: The (envelope) address sending this mail. |
|---|
| 1740 |
@param to_addrs: A list of addresses to send this mail to. A string will |
|---|
| 1741 |
be treated as a list of one address |
|---|
| 1742 |
@param msg: The message, including headers, either as a file or a string. |
|---|
| 1743 |
File-like objects need to support read() and close(). Lines must be |
|---|
| 1744 |
delimited by '\\n'. If you pass something that doesn't look like a |
|---|
| 1745 |
file, we try to convert it to a string (so you should be able to |
|---|
| 1746 |
pass an email.Message directly, but doing the conversion with |
|---|
| 1747 |
email.Generator manually will give you more control over the |
|---|
| 1748 |
process). |
|---|
| 1749 |
|
|---|
| 1750 |
@param senderDomainName: Name by which to identify. If None, try |
|---|
| 1751 |
to pick something sane (but this depends on external configuration |
|---|
| 1752 |
and may not succeed). |
|---|
| 1753 |
|
|---|
| 1754 |
@param port: Remote port to which to connect. |
|---|
| 1755 |
|
|---|
| 1756 |
@rtype: L{Deferred} |
|---|
| 1757 |
@returns: A L{Deferred}, its callback will be called if a message is sent |
|---|
| 1758 |
to ANY address, the errback if no message is sent. |
|---|
| 1759 |
|
|---|
| 1760 |
The callback will be called with a tuple (numOk, addresses) where numOk |
|---|
| 1761 |
is the number of successful recipient addresses and addresses is a list |
|---|
| 1762 |
of tuples (address, code, resp) giving the response to the RCPT command |
|---|
| 1763 |
for each address. |
|---|
| 1764 |
""" |
|---|
| 1765 |
if not hasattr(msg,'read'): |
|---|
| 1766 |
|
|---|
| 1767 |
msg = StringIO(str(msg)) |
|---|
| 1768 |
|
|---|
| 1769 |
d = defer.Deferred() |
|---|
| 1770 |
factory = SMTPSenderFactory(from_addr, to_addrs, msg, d) |
|---|
| 1771 |
|
|---|
| 1772 |
if senderDomainName is not None: |
|---|
| 1773 |
factory.domain = senderDomainName |
|---|
| 1774 |
|
|---|
| 1775 |
reactor.connectTCP(smtphost, port, factory) |
|---|
| 1776 |
|
|---|
| 1777 |
return d |
|---|
| 1778 |
|
|---|
| 1779 |
def sendEmail(smtphost, fromEmail, toEmail, content, headers = None, attachments = None, multipartbody = "mixed"): |
|---|
| 1780 |
"""Send an email, optionally with attachments. |
|---|
| 1781 |
|
|---|
| 1782 |
@type smtphost: str |
|---|
| 1783 |
@param smtphost: hostname of SMTP server to which to connect |
|---|
| 1784 |
|
|---|
| 1785 |
@type fromEmail: str |
|---|
| 1786 |
@param fromEmail: email address to indicate this email is from |
|---|
| 1787 |
|
|---|
| 1788 |
@type toEmail: str |
|---|
| 1789 |
@param toEmail: email address to which to send this email |
|---|
| 1790 |
|
|---|
| 1791 |
@type content: str |
|---|
| 1792 |
@param content: The body if this email. |
|---|
| 1793 |
|
|---|
| 1794 |
@type headers: dict |
|---|
| 1795 |
@param headers: Dictionary of headers to include in the email |
|---|
| 1796 |
|
|---|
| 1797 |
@type attachments: list of 3-tuples |
|---|
| 1798 |
@param attachments: Each 3-tuple should consist of the name of the |
|---|
| 1799 |
attachment, the mime-type of the attachment, and a string that is |
|---|
| 1800 |
the attachment itself. |
|---|
| 1801 |
|
|---|
| 1802 |
@type multipartbody: str |
|---|
| 1803 |
@param multipartbody: The type of MIME multi-part body. Generally |
|---|
| 1804 |
either "mixed" (as in text and images) or "alternative" (html email |
|---|
| 1805 |
with a fallback to text/plain). |
|---|
| 1806 |
|
|---|
| 1807 |
@rtype: Deferred |
|---|
| 1808 |
@return: The returned Deferred has its callback or errback invoked when |
|---|
| 1809 |
the mail is successfully sent or when an error occurs, respectively. |
|---|
| 1810 |
""" |
|---|
| 1811 |
warnings.warn("smtp.sendEmail may go away in the future.\n" |
|---|
| 1812 |
" Consider revising your code to use the email module\n" |
|---|
| 1813 |
" and smtp.sendmail.", |
|---|
| 1814 |
category=DeprecationWarning, stacklevel=2) |
|---|
| 1815 |
|
|---|
| 1816 |
f = tempfile.TemporaryFile() |
|---|
| 1817 |
writer = MimeWriter.MimeWriter(f) |
|---|
| 1818 |
|
|---|
| 1819 |
writer.addheader("Mime-Version", "1.0") |
|---|
| 1820 |
if headers: |
|---|
| 1821 |
|
|---|
| 1822 |
for (header, value) in headers.items(): |
|---|
| 1823 |
writer.addheader(header, value) |
|---|
| 1824 |
|
|---|
| 1825 |
headkeys = [k.lower() for k in headers.keys()] |
|---|
| 1826 |
else: |
|---|
| 1827 |
headkeys = () |
|---|
| 1828 |
|
|---|
| 1829 |
|
|---|
| 1830 |
if "message-id" not in headkeys: |
|---|
| 1831 |
writer.addheader("Message-ID", messageid()) |
|---|
| 1832 |
if "date" not in headkeys: |
|---|
| 1833 |
writer.addheader("Date", rfc822date()) |
|---|
| 1834 |
if "from" not in headkeys and "sender" not in headkeys: |
|---|
| 1835 |
writer.addheader("From", fromEmail) |
|---|
| 1836 |
if "to" not in headkeys and "cc" not in headkeys and "bcc" not in headkeys: |
|---|
| 1837 |
writer.addheader("To", toEmail) |
|---|
| 1838 |
|
|---|
| 1839 |
writer.startmultipartbody(multipartbody) |
|---|
| 1840 |
|
|---|
| 1841 |
|
|---|
| 1842 |
part = writer.nextpart() |
|---|
| 1843 |
body = part.startbody("text/plain") |
|---|
| 1844 |
body.write(content) |
|---|
| 1845 |
|
|---|
| 1846 |
if attachments is not None: |
|---|
| 1847 |
|
|---|
| 1848 |
for (file, mime, attachment) in attachments: |
|---|
| 1849 |
part = writer.nextpart() |
|---|
| 1850 |
if mime.startswith('text'): |
|---|
| 1851 |
encoding = "7bit" |
|---|
| 1852 |
else: |
|---|
| 1853 |
attachment = base64.encodestring(attachment) |
|---|
| 1854 |
encoding = "base64" |
|---|
| 1855 |
part.addheader("Content-Transfer-Encoding", encoding) |
|---|
| 1856 |
body = part.startbody("%s; name=%s" % (mime, file)) |
|---|
| 1857 |
body.write(attachment) |
|---|
| 1858 |
|
|---|
| 1859 |
|
|---|
| 1860 |
writer.lastpart() |
|---|
| 1861 |
|
|---|
| 1862 |
|
|---|
| 1863 |
f.seek(0, 0) |
|---|
| 1864 |
d = defer.Deferred() |
|---|
| 1865 |
factory = SMTPSenderFactory(fromEmail, toEmail, f, d) |
|---|
| 1866 |
reactor.connectTCP(smtphost, 25, factory) |
|---|
| 1867 |
|
|---|
| 1868 |
return d |
|---|
| 1869 |
|
|---|
| 1870 |
|
|---|
| 1871 |
|
|---|
| 1872 |
|
|---|
| 1873 |
import codecs |
|---|
| 1874 |
def xtext_encode(s): |
|---|
| 1875 |
r = [] |
|---|
| 1876 |
for ch in s: |
|---|
| 1877 |
o = ord(ch) |
|---|
| 1878 |
if ch == '+' or ch == '=' or o < 33 or o > 126: |
|---|
| 1879 |
r.append('+%02X' % o) |
|---|
| 1880 |
else: |
|---|
| 1881 |
r.append(ch) |
|---|
| 1882 |
return (''.join(r), len(s)) |
|---|
| 1883 |
|
|---|
| 1884 |
try: |
|---|
| 1885 |
from twisted.protocols._c_urlarg import unquote as _helper_unquote |
|---|
| 1886 |
except ImportError: |
|---|
| 1887 |
def xtext_decode(s): |
|---|
| 1888 |
r = [] |
|---|
| 1889 |
i = 0 |
|---|
| 1890 |
while i < len(s): |
|---|
| 1891 |
if s[i] == '+': |
|---|
| 1892 |
try: |
|---|
| 1893 |
r.append(chr(int(s[i + 1:i + 3], 16))) |
|---|
| 1894 |
except ValueError: |
|---|
| 1895 |
r.append(s[i:i + 3]) |
|---|
| 1896 |
i += 3 |
|---|
| 1897 |
else: |
|---|
| 1898 |
r.append(s[i]) |
|---|
| 1899 |
i += 1 |
|---|
| 1900 |
return (''.join(r), len(s)) |
|---|
| 1901 |
else: |
|---|
| 1902 |
def xtext_decode(s): |
|---|
| 1903 |
return (_helper_unquote(s, '+'), len(s)) |
|---|
| 1904 |
|
|---|
| 1905 |
class xtextStreamReader(codecs.StreamReader): |
|---|
| 1906 |
def decode(self, s, errors='strict'): |
|---|
| 1907 |
return xtext_decode(s) |
|---|
| 1908 |
|
|---|
| 1909 |
class xtextStreamWriter(codecs.StreamWriter): |
|---|
| 1910 |
def decode(self, s, errors='strict'): |
|---|
| 1911 |
return xtext_encode(s) |
|---|
| 1912 |
|
|---|
| 1913 |
def xtext_codec(name): |
|---|
| 1914 |
if name == 'xtext': |
|---|
| 1915 |
return (xtext_encode, xtext_decode, xtextStreamReader, xtextStreamWriter) |
|---|
| 1916 |
codecs.register(xtext_codec) |
|---|