Guest User

Untitled

a guest
Jan 20th, 2018
119
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
text 20.03 KB | None | 0 0
  1. """
  2. ###############################################################################
  3. send messages, add attachments (see __init__ for docs, test)
  4. ###############################################################################
  5. """
  6.  
  7. import mailconfig # client's mailconfig
  8. import smtplib, os, mimetypes # mime: name to type
  9. import email.utils, email.encoders # date string, base64
  10. from .mailTool import MailTool, SilentMailTool # 4E: package-relative
  11.  
  12. from email.message import Message # general message, obj->text
  13. from email.mime.multipart import MIMEMultipart # type-specific messages
  14. from email.mime.audio import MIMEAudio # format/encode attachments
  15. from email.mime.image import MIMEImage
  16. from email.mime.text import MIMEText
  17. from email.mime.base import MIMEBase
  18. from email.mime.application import MIMEApplication # 4E: use new app class
  19.  
  20.  
  21. def fix_encode_base64(msgobj):
  22. """
  23. 4E: workaround for a genuine bug in Python 3.1 email package that prevents
  24. mail text generation for binary parts encoded with base64 or other email
  25. encodings; the normal email.encoder run by the constructor leaves payload
  26. as bytes, even though it's encoded to base64 text form; this breaks email
  27. text generation which assumes this is text and requires it to be str; net
  28. effect is that only simple text part emails can be composed in Py 3.1 email
  29. package as is - any MIME-encoded binary part cause mail text generation to
  30. fail; this bug seems likely to go away in a future Python and email package,
  31. in which case this should become a no-op; see Chapter 13 for more details;
  32. """
  33.  
  34. linelen = 76 # per MIME standards
  35. from email.encoders import encode_base64
  36.  
  37. encode_base64(msgobj) # what email does normally: leaves bytes
  38. text = msgobj.get_payload() # bytes fails in email pkg on text gen
  39. if isinstance(text, bytes): # payload is bytes in 3.1, str in 3.2 alpha
  40. text = text.decode('ascii') # decode to unicode str so text gen works
  41.  
  42. lines = [] # split into lines, else 1 massive line
  43. text = text.replace('n', '') # no n present in 3.1, but futureproof me!
  44. while text:
  45. line, text = text[:linelen], text[linelen:]
  46. lines.append(line)
  47. msgobj.set_payload('n'.join(lines))
  48.  
  49.  
  50. def fix_text_required(encodingname):
  51. """
  52. 4E: workaround for str/bytes combination errors in email package; MIMEText
  53. requires different types for different Unicode encodings in Python 3.1, due
  54. to the different ways it MIME-encodes some types of text; see Chapter 13;
  55. the only other alternative is using generic Message and repeating much code;
  56. """
  57. from email.charset import Charset, BASE64, QP
  58.  
  59. charset = Charset(encodingname) # how email knows what to do for encoding
  60. bodyenc = charset.body_encoding # utf8, others require bytes input data
  61. return bodyenc in (None, QP) # ascii, latin1, others require str
  62.  
  63.  
  64. class MailSender(MailTool):
  65. """
  66. send mail: format a message, interface with an SMTP server;
  67. works on any machine with Python+Inet, doesn't use cmdline mail;
  68. a nonauthenticating client: see MailSenderAuth if login required;
  69. 4E: tracesize is num chars of msg text traced: 0=none, big=all;
  70. 4E: supports Unicode encodings for main text and text parts;
  71. 4E: supports header encoding, both full headers and email names;
  72. """
  73. def __init__(self, smtpserver=None, tracesize=256):
  74. self.smtpServerName = smtpserver or mailconfig.smtpservername
  75. self.tracesize = tracesize
  76.  
  77. def sendMessage(self, From, To, Subj, extrahdrs, bodytext, attaches,
  78. saveMailSeparator=(('=' * 80) + 'PYn'),
  79. bodytextEncoding='us-ascii',
  80. attachesEncodings=None):
  81. """
  82. format and send mail: blocks caller, thread me in a GUI;
  83. bodytext is main text part, attaches is list of filenames,
  84. extrahdrs is list of (name, value) tuples to be added;
  85. raises uncaught exception if send fails for any reason;
  86. saves sent message text in a local file if successful;
  87.  
  88. assumes that To, Cc, Bcc hdr values are lists of 1 or more already
  89. decoded addresses (possibly in full name+<addr> format); client
  90. must parse to split these on delimiters, or use multiline input;
  91. note that SMTP allows full name+<addr> format in recipients;
  92. 4E: Bcc addrs now used for send/envelope, but header is dropped;
  93. 4E: duplicate recipients removed, else will get >1 copies of mail;
  94. caveat: no support for multipart/alternative mails, just /mixed;
  95. """
  96.  
  97. # 4E: assume main body text is already in desired encoding;
  98. # clients can decode to user pick, default, or utf8 fallback;
  99. # either way, email needs either str xor bytes specifically;
  100.  
  101. if fix_text_required(bodytextEncoding):
  102. if not isinstance(bodytext, str):
  103. bodytext = bodytext.decode(bodytextEncoding)
  104. else:
  105. if not isinstance(bodytext, bytes):
  106. bodytext = bodytext.encode(bodytextEncoding)
  107.  
  108. # make message root
  109. if not attaches:
  110. msg = Message()
  111. msg.set_payload(bodytext, charset=bodytextEncoding)
  112. else:
  113. msg = MIMEMultipart()
  114. self.addAttachments(msg, bodytext, attaches,
  115. bodytextEncoding, attachesEncodings)
  116.  
  117. # 4E: non-ASCII hdrs encoded on sends; encode just name in address,
  118. # else smtp may drop the message completely; encodes all envelope
  119. # To names (but not addr) also, and assumes servers will allow;
  120. # msg.as_string retains any line breaks added by encoding headers;
  121.  
  122. hdrenc = mailconfig.headersEncodeTo or 'utf-8' # default=utf8
  123. Subj = self.encodeHeader(Subj, hdrenc) # full header
  124. From = self.encodeAddrHeader(From, hdrenc) # email names
  125. To = [self.encodeAddrHeader(T, hdrenc) for T in To] # each recip
  126. Tos = ', '.join(To) # hdr+envelope
  127.  
  128. # add headers to root
  129. msg['From'] = From
  130. msg['To'] = Tos # poss many: addr list
  131. msg['Subject'] = Subj # servers reject ';' sept
  132. msg['Date'] = email.utils.formatdate() # curr datetime, rfc2822 utc
  133. recip = To
  134. for name, value in extrahdrs: # Cc, Bcc, X-Mailer, etc.
  135. if value:
  136. if name.lower() not in ['cc', 'bcc']:
  137. value = self.encodeHeader(value, hdrenc)
  138. msg[name] = value
  139. else:
  140. value = [self.encodeAddrHeader(V, hdrenc) for V in value]
  141. recip += value # some servers reject ['']
  142. if name.lower() != 'bcc': # 4E: bcc gets mail, no hdr
  143. msg[name] = ', '.join(value) # add commas between cc
  144.  
  145. recip = list(set(recip)) # 4E: remove duplicates
  146. fullText = msg.as_string() # generate formatted msg
  147.  
  148. # sendmail call raises except if all Tos failed,
  149. # or returns failed Tos dict for any that failed
  150.  
  151. self.trace('Sending to...' + str(recip))
  152. self.trace(fullText[:self.tracesize]) # SMTP calls connect
  153. server = smtplib.SMTP(self.smtpServerName, timeout=20) # this may fail too
  154. self.getPassword() # if srvr requires
  155. self.authenticateServer(server) # login in subclass
  156. try:
  157. failed = server.sendmail(From, recip, fullText) # except or dict
  158. except:
  159. server.close() # 4E: quit may hang!
  160. raise # reraise except
  161. else:
  162. server.quit() # connect + send OK
  163. self.saveSentMessage(fullText, saveMailSeparator) # 4E: do this first
  164. if failed:
  165. class SomeAddrsFailed(Exception): pass
  166. raise SomeAddrsFailed('Failed addrs:%sn' % failed)
  167. self.trace('Send exit')
  168.  
  169. def addAttachments(self, mainmsg, bodytext, attaches,
  170. bodytextEncoding, attachesEncodings):
  171. """
  172. format a multipart message with attachments;
  173. use Unicode encodings for text parts if passed;
  174. """
  175. # add main text/plain part
  176. msg = MIMEText(bodytext, _charset=bodytextEncoding)
  177. mainmsg.attach(msg)
  178.  
  179. # add attachment parts
  180. encodings = attachesEncodings or (['us-ascii'] * len(attaches))
  181. for (filename, fileencode) in zip(attaches, encodings):
  182. # filename may be absolute or relative
  183. if not os.path.isfile(filename): # skip dirs, etc.
  184. continue
  185.  
  186. # guess content type from file extension, ignore encoding
  187. contype, encoding = mimetypes.guess_type(filename)
  188. if contype is None or encoding is not None: # no guess, compressed?
  189. contype = 'application/octet-stream' # use generic default
  190. self.trace('Adding ' + contype)
  191.  
  192. # build sub-Message of appropriate kind
  193. maintype, subtype = contype.split('/', 1)
  194. if maintype == 'text': # 4E: text needs encoding
  195. if fix_text_required(fileencode): # requires str or bytes
  196. data = open(filename, 'r', encoding=fileencode)
  197. else:
  198. data = open(filename, 'rb')
  199. msg = MIMEText(data.read(), _subtype=subtype, _charset=fileencode)
  200. data.close()
  201.  
  202. elif maintype == 'image':
  203. data = open(filename, 'rb') # 4E: use fix for binaries
  204. msg = MIMEImage(
  205. data.read(), _subtype=subtype, _encoder=fix_encode_base64)
  206. data.close()
  207.  
  208. elif maintype == 'audio':
  209. data = open(filename, 'rb')
  210. msg = MIMEAudio(
  211. data.read(), _subtype=subtype, _encoder=fix_encode_base64)
  212. data.close()
  213.  
  214. elif maintype == 'application': # new in 4E
  215. data = open(filename, 'rb')
  216. msg = MIMEApplication(
  217. data.read(), _subtype=subtype, _encoder=fix_encode_base64)
  218. data.close()
  219.  
  220. else:
  221. data = open(filename, 'rb') # application/* could
  222. msg = MIMEBase(maintype, subtype) # use this code too
  223. msg.set_payload(data.read())
  224. data.close() # make generic type
  225. fix_encode_base64(msg) # was broken here too!
  226. #email.encoders.encode_base64(msg) # encode using base64
  227.  
  228. # set filename (ascii or utf8/mime encoded) and attach to container
  229. basename = self.encodeHeader(os.path.basename(filename)) # oct 2011
  230. msg.add_header('Content-Disposition',
  231. 'attachment', filename=basename)
  232. mainmsg.attach(msg)
  233.  
  234. # text outside mime structure, seen by non-MIME mail readers
  235. mainmsg.preamble = 'A multi-part MIME format message.n'
  236. mainmsg.epilogue = '' # make sure message ends with a newline
  237.  
  238. def saveSentMessage(self, fullText, saveMailSeparator):
  239. """
  240. append sent message to local file if send worked for any;
  241. client: pass separator used for your application, splits;
  242. caveat: user may change the file at same time (unlikely);
  243. """
  244. try:
  245. sentfile = open(mailconfig.sentmailfile, 'a',
  246. encoding=mailconfig.fetchEncoding) # 4E
  247. if fullText[-1] != 'n': fullText += 'n'
  248. sentfile.write(saveMailSeparator)
  249. sentfile.write(fullText)
  250. sentfile.close()
  251. except:
  252. self.trace('Could not save sent message') # not a show-stopper
  253.  
  254. def encodeHeader(self, headertext, unicodeencoding='utf-8'):
  255. """
  256. 4E: encode composed non-ascii message headers content per both email
  257. and Unicode standards, according to an optional user setting or UTF-8;
  258. header.encode adds line breaks in header string automatically if needed;
  259. """
  260. try:
  261. headertext.encode('ascii')
  262. except:
  263. try:
  264. hdrobj = email.header.make_header([(headertext, unicodeencoding)])
  265. headertext = hdrobj.encode()
  266. except:
  267. pass # auto splits into multiple cont lines if needed
  268. return headertext # smtplib may fail if it won't encode to ascii
  269.  
  270. def encodeAddrHeader(self, headertext, unicodeencoding='utf-8'):
  271. """
  272. 4E: try to encode non-ASCII names in email addresess per email, MIME,
  273. and Unicode standards; if this fails drop name and use just addr part;
  274. if cannot even get addresses, try to decode as a whole, else smtplib
  275. may run into errors when it tries to encode the entire mail as ASCII;
  276. utf-8 default should work for most, as it formats code points broadly;
  277.  
  278. inserts newlines if too long or hdr.encode split names to multiple lines,
  279. but this may not catch some lines longer than the cutoff (improve me);
  280. as used, Message.as_string formatter won't try to break lines further;
  281. see also decodeAddrHeader in mailParser module for the inverse of this;
  282. """
  283. try:
  284. pairs = email.utils.getaddresses([headertext]) # split addrs + parts
  285. encoded = []
  286. for name, addr in pairs:
  287. try:
  288. name.encode('ascii') # use as is if okay as ascii
  289. except UnicodeError: # else try to encode name part
  290. try:
  291. uni = name.encode(unicodeencoding)
  292. hdr = email.header.make_header([(uni, unicodeencoding)])
  293. name = hdr.encode()
  294. except:
  295. name = None # drop name, use address part only
  296. joined = email.utils.formataddr((name, addr)) # quote name if need
  297. encoded.append(joined)
  298.  
  299. fullhdr = ', '.join(encoded)
  300. if len(fullhdr) > 72 or 'n' in fullhdr: # not one short line?
  301. fullhdr = ',n '.join(encoded) # try multiple lines
  302. return fullhdr
  303. except:
  304. return self.encodeHeader(headertext)
  305.  
  306. def authenticateServer(self, server):
  307. pass # no login required for this server/class
  308.  
  309. def getPassword(self):
  310. pass # no login required for this server/class
  311.  
  312.  
  313. ################################################################################
  314. # specialized subclasses
  315. ################################################################################
  316.  
  317. class MailSenderAuth(MailSender):
  318. """
  319. use for servers that require login authorization;
  320. client: choose MailSender or MailSenderAuth super
  321. class based on mailconfig.smtpuser setting (None?)
  322. """
  323. smtpPassword = None # 4E: on class, not self, shared by poss N instances
  324.  
  325. def __init__(self, smtpserver=None, smtpuser=None):
  326. MailSender.__init__(self, smtpserver)
  327. self.smtpUser = smtpuser or mailconfig.smtpuser
  328. # self.smtpPassword = None # 4E: makes pyMailGUI ask for each send!
  329.  
  330. def authenticateServer(self, server):
  331. server.login(self.smtpUser, self.smtpPassword)
  332.  
  333. def getPassword(self):
  334. """
  335. get SMTP auth password if not yet known;
  336. may be called by superclass auto, or client manual:
  337. not needed until send, but don't run in GUI thread;
  338. get from client-side file or subclass method
  339. """
  340. if not self.smtpPassword:
  341. try:
  342. localfile = open(mailconfig.smtppasswdfile)
  343. MailSenderAuth.smtpPassword = localfile.readline()[:-1] # 4E
  344. self.trace('local file password' + repr(self.smtpPassword))
  345. except:
  346. MailSenderAuth.smtpPassword = self.askSmtpPassword() # 4E
  347.  
  348. def askSmtpPassword(self):
  349. assert False, 'Subclass must define method'
  350.  
  351. class MailSenderAuthConsole(MailSenderAuth):
  352. def askSmtpPassword(self):
  353. import getpass
  354. prompt = 'Password for %s on %s?' % (self.smtpUser, self.smtpServerName)
  355. return getpass.getpass(prompt)
  356.  
  357. class SilentMailSender(SilentMailTool, MailSender):
  358. pass # replaces trace
  359.  
  360. """
  361. ###############################################################################
  362. self-test when this file is run as a program
  363. ###############################################################################
  364. """
  365.  
  366. #
  367. # mailconfig normally comes from the client's source directory or
  368. # sys.path; for testing, get it from Email directory one level up
  369. #
  370. import sys
  371. sys.path.append('..')
  372. import mailconfig
  373. print('config:', mailconfig.__file__)
  374.  
  375. # get these from __init__
  376. from mailtools import (MailFetcherConsole,
  377. MailSender, MailSenderAuthConsole,
  378. MailParser)
  379.  
  380. if not mailconfig.smtpuser:
  381. sender = MailSender(tracesize=5000)
  382. else:
  383. sender = MailSenderAuthConsole(tracesixe=5000)
  384.  
  385. sender.sendMessage(From = mailconfig.myaddress,
  386. To = [mailconfig.myaddress],
  387. Subj = 'testing mailtools package',
  388. extrahdrs = [('X-Mailer', 'mailtools')],
  389. bodytext = 'Here is my source coden',
  390. attaches = ['selftest.py'],
  391. )
  392.  
  393. # bodytextEncoding='utf-8', # other tests to try
  394. # attachesEncodings=['latin-1'], # inspect text headers
  395. # attaches=['monkeys.jpg']) # verify Base64 encoded
  396. # to='i18n adddr list...', # test mime/unicode headers
  397.  
  398.  
  399. # change mailconfig to test fetchlimit
  400. fetcher = MailFetcherConsole()
  401. def status(*args): print(args)
  402.  
  403. hdrs, sizes, loadedall = fetcher.downloadAllHeaders(status)
  404. for num, hdr in enumerate(hdrs[:5]):
  405. print(hdr)
  406. if input('load mail?') in ['y', 'Y']:
  407. print(fetcher.downloadMessage(num+1).rstrip(), 'n', '-'*70)
  408.  
  409. last5 = len(hdrs)-4
  410. msgs, sizes, loadedall = fetcher.downloadAllMessages(status, loadfrom=last5)
  411. for msg in msgs:
  412. print(msg[:200], 'n', '-'*70)
  413.  
  414. parser = MailParser()
  415. for i in [0]: # try [0 , len(msgs)]
  416. fulltext = msgs[i]
  417. message = parser.parseMessage(fulltext)
  418. ctype, maintext = parser.findMainText(message)
  419. print('Parsed:', message['Subject'])
  420. print(maintext)
  421. input('Press Enter to exit') # pause if clicked on Windows
  422.  
  423. Traceback (most recent call last):
  424. File "C:UsersАлексейAppDataLocalProgramsPythonPython36-32PP4EInternetEmailmailtoolsselftest.py", line 24, in <module>
  425. sender = MailSenderAuthConsole(tracesize=5000)
  426. TypeError: __init__() got an unexpected keyword argument 'tracesize'
  427.  
  428. Traceback (most recent call last):
  429. File "C:UsersАлексейAppDataLocalProgramsPythonPython36-32PP4EInternetEmailmailtoolsselftest.py", line 31, in <module>
  430. attaches = ['selftest.py'],
  431. File "..mailtoolsmailSender.py", line 155, in sendMessage
  432. self.authenticateServer(server) # login in subclass
  433. File "..mailtoolsmailSender.py", line 331, in authenticateServer
  434. server.login(self.smtpUser, self.smtpPassword)
  435. File "C:UsersАлексейAppDataLocalProgramsPythonPython36-32libsmtplib.py", line 721, in login
  436. initial_response_ok=initial_response_ok)
  437. File "C:UsersАлексейAppDataLocalProgramsPythonPython36-32libsmtplib.py", line 630, in auth
  438. response = encode_base64(initial_response.encode('ascii'), eol='')
  439. AttributeError: 'bool' object has no attribute 'encode'
Add Comment
Please, Sign In to add comment