You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 

480 lines
17 KiB

  1. # reportbug_submit module - email and GnuPG functions
  2. # Written by Chris Lawrence <lawrencc@debian.org>
  3. # Copyright (C) 1999-2006 Chris Lawrence
  4. # Copyright (C) 2008-2017 Sandro Tosi <morph@debian.org>
  5. #
  6. # This program is freely distributable per the following license:
  7. #
  8. # Permission to use, copy, modify, and distribute this software and its
  9. # documentation for any purpose and without fee is hereby granted,
  10. # provided that the above copyright notice appears in all copies and that
  11. # both that copyright notice and this permission notice appear in
  12. # supporting documentation.
  13. #
  14. # I DISCLAIM ALL WARRANTIES WITH REGARD TO THIS SOFTWARE, INCLUDING ALL
  15. # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS, IN NO EVENT SHALL I
  16. # BE LIABLE FOR ANY SPECIAL, INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY
  17. # DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS,
  18. # WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION,
  19. # ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS
  20. # SOFTWARE.
  21. import sys
  22. import os
  23. import re
  24. import shlex
  25. from subprocess import Popen, STDOUT, PIPE
  26. import email
  27. import smtplib
  28. import socket
  29. import email
  30. from email.mime.multipart import MIMEMultipart
  31. from email.mime.text import MIMEText
  32. from email.mime.audio import MIMEAudio
  33. from email.mime.image import MIMEImage
  34. from email.mime.base import MIMEBase
  35. from email.mime.message import MIMEMessage
  36. import mimetypes
  37. from .__init__ import VERSION, VERSION_NUMBER
  38. from .tempfiles import TempFile, open_write_safe, tempfile_prefix
  39. from .exceptions import (
  40. NoMessage,
  41. )
  42. from .ui import text_ui as ui
  43. from .utils import get_email_addr
  44. import errno
  45. quietly = False
  46. ascii_range = ''.join([chr(ai) for ai in range(32, 127)])
  47. notascii = re.compile(r'[^' + re.escape(ascii_range) + ']')
  48. notascii2 = re.compile(r'[^' + re.escape(ascii_range) + r'\s]')
  49. # Cheat for now.
  50. # ewrite() may put stuff on the status bar or in message boxes depending on UI
  51. def ewrite(*args):
  52. return quietly or ui.log_message(*args)
  53. def sign_message(body, fromaddr, package='x', pgp_addr=None, sign='gpg', draftpath=None):
  54. '''Sign message with pgp key.'''
  55. ''' Return: a signed body.
  56. On failure, return None.
  57. kw need to have the following keys
  58. '''
  59. if not pgp_addr:
  60. pgp_addr = get_email_addr(fromaddr)[1]
  61. # Make the unsigned file first
  62. (unsigned, file1) = TempFile(prefix=tempfile_prefix(package, 'unsigned'), dir=draftpath)
  63. unsigned.write(body)
  64. unsigned.close()
  65. # Now make the signed file
  66. (signed, file2) = TempFile(prefix=tempfile_prefix(package, 'signed'), dir=draftpath)
  67. signed.close()
  68. if sign == 'gpg':
  69. os.unlink(file2)
  70. if 'GPG_AGENT_INFO' not in os.environ:
  71. signcmd = "gpg --local-user '%s' --clearsign " % pgp_addr
  72. else:
  73. signcmd = "gpg --local-user '%s' --use-agent --clearsign " % pgp_addr
  74. signcmd += '--output ' + shlex.quote(file2) + ' ' + shlex.quote(file1)
  75. else:
  76. signcmd = "pgp -u '%s' -fast" % pgp_addr
  77. signcmd += '<' + shlex.quote(file1) + ' >' + shlex.quote(file2)
  78. try:
  79. os.system(signcmd)
  80. x = open(file2, 'r')
  81. signedbody = x.read()
  82. x.close()
  83. if os.path.exists(file1):
  84. os.unlink(file1)
  85. if os.path.exists(file2):
  86. os.unlink(file2)
  87. if not signedbody:
  88. raise NoMessage
  89. body = signedbody
  90. except (NoMessage, IOError, OSError):
  91. fh, tmpfile2 = TempFile(prefix=tempfile_prefix(package), dir=draftpath)
  92. fh.write(body)
  93. fh.close()
  94. ewrite('gpg/pgp failed; input file in %s\n', tmpfile2)
  95. body = None
  96. return body
  97. def mime_attach(body, attachments, charset, body_charset=None):
  98. mimetypes.init()
  99. message = MIMEMultipart('mixed')
  100. bodypart = MIMEText(body)
  101. bodypart.add_header('Content-Disposition', 'inline')
  102. message.preamble = 'This is a multi-part MIME message sent by reportbug.\n\n'
  103. message.epilogue = ''
  104. message.attach(bodypart)
  105. failed = False
  106. for attachment in attachments:
  107. try:
  108. fp = open(attachment)
  109. fp.close()
  110. except EnvironmentError as x:
  111. ewrite("Warning: opening '%s' failed: %s.\n", attachment,
  112. x.strerror)
  113. failed = True
  114. continue
  115. ctype = None
  116. cset = charset
  117. info = Popen(['file', '--mime', '--brief', attachment],
  118. stdout=PIPE, stderr=STDOUT).communicate()[0].decode('ascii')
  119. print(info)
  120. if info:
  121. match = re.match(r'([^;, ]*)(,[^;]+)?(?:; )?(.*)', info)
  122. if match:
  123. ctype, junk, extras = match.groups()
  124. match = re.search(r'charset=([^,]+|"[^,"]+")', extras)
  125. if match:
  126. cset = match.group(1)
  127. # If we didn't get a real MIME type, fall back
  128. if '/' not in ctype:
  129. ctype = None
  130. # If file doesn't work, try to guess based on the extension
  131. if not ctype:
  132. ctype, encoding = mimetypes.guess_type(
  133. attachment, strict=False)
  134. if not ctype:
  135. ctype = 'application/octet-stream'
  136. maintype, subtype = ctype.split('/', 1)
  137. if maintype == 'text':
  138. fp = open(attachment, 'rU')
  139. part = MIMEText(fp.read())
  140. fp.close()
  141. elif maintype == 'message':
  142. fp = open(attachment, 'rb')
  143. part = MIMEMessage(email.message_from_file(fp),
  144. _subtype=subtype)
  145. fp.close()
  146. elif maintype == 'image':
  147. fp = open(attachment, 'rb')
  148. part = MIMEImage(fp.read(), _subtype=subtype)
  149. fp.close()
  150. elif maintype == 'audio':
  151. fp = open(attachment, 'rb')
  152. part = MIMEAudio(fp.read(), _subtype=subtype)
  153. fp.close()
  154. else:
  155. fp = open(attachment, 'rb')
  156. part = MIMEBase(maintype, subtype)
  157. part.set_payload(fp.read())
  158. fp.close()
  159. email.Encoders.encode_base64(part)
  160. part.add_header('Content-Disposition', 'attachment',
  161. filename=os.path.basename(attachment))
  162. message.attach(part)
  163. return (message, failed)
  164. def send_report(body, attachments, mua, fromaddr, sendto, ccaddr, bccaddr,
  165. headers, package='x', charset="us-ascii", mailing=True,
  166. sysinfo=None,
  167. rtype='debbugs', exinfo=None, replyto=None, printonly=False,
  168. template=False, outfile=None, mta='', kudos=False,
  169. smtptls=False, smtphost='localhost',
  170. smtpuser=None, smtppasswd=None, paranoid=False, draftpath=None,
  171. envelopefrom=None):
  172. '''Send a report.'''
  173. failed = using_sendmail = False
  174. msgname = ''
  175. # Disable smtphost if mua is set
  176. if mua and smtphost:
  177. smtphost = ''
  178. # No, I'm not going to do a full MX lookup on every address... get a
  179. # real MTA!
  180. if kudos and smtphost == 'reportbug.debian.org':
  181. smtphost = 'packages.debian.org'
  182. body_charset = 'utf-8'
  183. tfprefix = tempfile_prefix(package)
  184. if attachments and not mua:
  185. (message, failed) = mime_attach(body, attachments, charset, body_charset)
  186. if failed:
  187. ewrite("Error: Message creation failed, not sending\n")
  188. mua = mta = smtphost = None
  189. else:
  190. message = MIMEText(body)
  191. # Standard headers
  192. message['From'] = fromaddr
  193. message['To'] = sendto
  194. for (header, value) in headers:
  195. message[header] = value
  196. if ccaddr:
  197. message['Cc'] = ccaddr
  198. if bccaddr:
  199. message['Bcc'] = bccaddr
  200. replyto = os.environ.get("REPLYTO", replyto)
  201. if replyto:
  202. message['Reply-To'] = replyto
  203. if mailing:
  204. message['Message-ID'] = email.utils.make_msgid('reportbug')
  205. message['X-Mailer'] = VERSION
  206. message['Date'] = email.utils.formatdate(localtime=True)
  207. elif mua and not (printonly or template):
  208. message['X-Reportbug-Version'] = VERSION_NUMBER
  209. addrs = [str(x) for x in (message.get_all('To', []) +
  210. message.get_all('Cc', []) +
  211. message.get_all('Bcc', []))]
  212. alist = email.utils.getaddresses(addrs)
  213. cclist = [str(x) for x in message.get_all('X-Debbugs-Cc', [])]
  214. debbugs_cc = email.utils.getaddresses(cclist)
  215. if cclist:
  216. del message['X-Debbugs-Cc']
  217. addrlist = ', '.join(cclist)
  218. message['X-Debbugs-Cc'] = addrlist
  219. # Drop any Bcc headers from the message to be sent
  220. if not outfile and not mua:
  221. try:
  222. del message['Bcc']
  223. except:
  224. pass
  225. message = message.as_string()
  226. if paranoid and not (template or printonly):
  227. pager = os.environ.get('PAGER', 'sensible-pager')
  228. try:
  229. os.popen(pager, 'w').write(message)
  230. except Exception as e:
  231. # if the PAGER exits before all the text has been sent,
  232. # it'd send a SIGPIPE, so crash only if that's not the case
  233. if e.errno != errno.EPIPE:
  234. raise e
  235. if not ui.yes_no('Does your report seem satisfactory', 'Yes, send it.',
  236. 'No, don\'t send it.'):
  237. smtphost = mta = None
  238. filename = None
  239. if template or printonly:
  240. pipe = sys.stdout
  241. elif mua:
  242. pipe, filename = TempFile(prefix=tfprefix, dir=draftpath)
  243. elif outfile or not ((mta and os.path.exists(mta)) and not smtphost):
  244. # outfile can be None at this point
  245. if outfile:
  246. msgname = os.path.expanduser(outfile)
  247. else:
  248. msgname = '/var/tmp/%s.bug' % package
  249. if os.path.exists(msgname):
  250. try:
  251. os.rename(msgname, msgname + '~')
  252. except OSError:
  253. ewrite('Unable to rename existing %s as %s~\n',
  254. msgname, msgname)
  255. try:
  256. pipe = open_write_safe(msgname, 'w')
  257. except OSError:
  258. # we can't write to the selected file, use a temp file instead
  259. fh, newmsgname = TempFile(prefix=tfprefix, dir=draftpath)
  260. ewrite('Writing to %s failed; '
  261. 'using instead %s\n', msgname, newmsgname)
  262. msgname = newmsgname
  263. # we just need a place where to write() and a file handler
  264. # is here just for that
  265. pipe = fh
  266. elif (mta and os.path.exists(mta)) and not smtphost:
  267. try:
  268. x = os.getcwd()
  269. except OSError:
  270. os.chdir('/')
  271. malist = [shlex.quote(a[1]) for a in alist]
  272. jalist = ' '.join(malist)
  273. faddr = email.utils.parseaddr(fromaddr)[1]
  274. if envelopefrom:
  275. envfrom = email.utils.parseaddr(envelopefrom)[1]
  276. else:
  277. envfrom = faddr
  278. ewrite("Sending message via %s...\n", mta)
  279. pipe = os.popen('%s -f %s -oi -oem %s' % (
  280. mta, shlex.quote(envfrom), jalist), 'w')
  281. using_sendmail = True
  282. # saving a backup of the report
  283. backupfh, backupname = TempFile(prefix=tempfile_prefix(package, 'backup'), dir=draftpath)
  284. ewrite('Saving a backup of the report at %s\n', backupname)
  285. backupfh.write(message)
  286. backupfh.close()
  287. if smtphost:
  288. toaddrs = [x[1] for x in alist]
  289. tryagain = True
  290. refused = None
  291. retry = 0
  292. while tryagain:
  293. tryagain = False
  294. ewrite("Connecting to %s via SMTP...\n", smtphost)
  295. try:
  296. conn = None
  297. # if we're using reportbug.debian.org, send mail to
  298. # submit
  299. if smtphost.lower() == 'reportbug.debian.org':
  300. conn = smtplib.SMTP(smtphost, 587)
  301. else:
  302. conn = smtplib.SMTP(smtphost)
  303. response = conn.ehlo()
  304. if not (200 <= response[0] <= 299):
  305. conn.helo()
  306. if smtptls:
  307. conn.starttls()
  308. response = conn.ehlo()
  309. if not (200 <= response[0] <= 299):
  310. conn.helo()
  311. if smtpuser:
  312. if not smtppasswd:
  313. smtppasswd = ui.get_password(
  314. 'Enter SMTP password for %s@%s: ' %
  315. (smtpuser, smtphost))
  316. conn.login(smtpuser, smtppasswd)
  317. refused = conn.sendmail(fromaddr, toaddrs, message)
  318. conn.quit()
  319. except (socket.error, smtplib.SMTPException) as x:
  320. # If wrong password, try again...
  321. if isinstance(x, smtplib.SMTPAuthenticationError):
  322. ewrite('SMTP error: authentication failed. Try again.\n')
  323. tryagain = True
  324. smtppasswd = None
  325. retry += 1
  326. if retry <= 2:
  327. continue
  328. else:
  329. tryagain = False
  330. # In case of failure, ask to retry or to save & exit
  331. if ui.yes_no('SMTP send failure: %s. Do you want to retry (or else save the report and exit)?' % x,
  332. 'Yes, please retry.',
  333. 'No, save and exit.'):
  334. tryagain = True
  335. continue
  336. else:
  337. failed = True
  338. fh, msgname = TempFile(prefix=tfprefix, dir=draftpath)
  339. fh.write(message)
  340. fh.close()
  341. ewrite('Wrote bug report to %s\n', msgname)
  342. # Handle when some recipients are refused.
  343. if refused:
  344. for (addr, err) in refused.items():
  345. ewrite('Unable to send report to %s: %d %s\n', addr, err[0],
  346. err[1])
  347. fh, msgname = TempFile(prefix=tfprefix, dir=draftpath)
  348. fh.write(message)
  349. fh.close()
  350. ewrite('Wrote bug report to %s\n', msgname)
  351. else:
  352. try:
  353. pipe.write(message)
  354. pipe.flush()
  355. if msgname:
  356. ewrite("Bug report written as %s\n", msgname)
  357. except IOError:
  358. failed = True
  359. pipe.close()
  360. if failed or (pipe.close() and using_sendmail):
  361. failed = True
  362. fh, msgname = TempFile(prefix=tfprefix, dir=draftpath)
  363. fh.write(message)
  364. fh.close()
  365. ui.long_message('Error: send/write operation failed, bug report '
  366. 'saved to %s\n', msgname)
  367. if mua:
  368. ewrite("Spawning %s...\n", mua.name)
  369. returnvalue = 0
  370. succeeded = False
  371. while not succeeded:
  372. returnvalue = mua.send(filename)
  373. if returnvalue != 0:
  374. ewrite("Mutt users should be aware it is mandatory to edit the draft before sending.\n")
  375. mtitle = 'Report has not been sent yet; what do you want to do now?'
  376. mopts = 'Eq'
  377. moptsdesc = {'e': 'Edit the message.',
  378. 'q': 'Quit reportbug; will save the draft for future use.'}
  379. x = ui.select_options(mtitle, mopts, moptsdesc)
  380. if x == 'q':
  381. failed = True
  382. fh, msgname = TempFile(prefix=tfprefix, dir=draftpath)
  383. fh.write(message)
  384. fh.close()
  385. ewrite('Draft saved into %s\n', msgname)
  386. succeeded = True
  387. else:
  388. succeeded = True
  389. elif not failed and (using_sendmail or smtphost):
  390. if kudos:
  391. ewrite('\nMessage sent to: %s\n', sendto)
  392. else:
  393. ewrite("\nBug report submitted to: %s\n", sendto)
  394. addresses = []
  395. for addr in alist:
  396. if addr[1] != email.utils.parseaddr(sendto)[1]:
  397. addresses.append(addr)
  398. if len(addresses):
  399. ewrite("Copies sent to:\n")
  400. for address in addrs:
  401. ewrite(' %s\n', address)
  402. if debbugs_cc and rtype == 'debbugs':
  403. ewrite("Copies will be sent after processing to:\n")
  404. for address in cclist:
  405. ewrite(' %s\n', address)
  406. if not (exinfo or kudos) and rtype == 'debbugs' and sysinfo and 'email' in sysinfo and not failed \
  407. and mailing:
  408. ewrite('\n')
  409. ui.final_message(
  410. """If you want to provide additional information, please wait to
  411. receive the bug tracking number via email; you may then send any extra
  412. information to %s (e.g. %s), where n is the bug number. Normally you
  413. will receive an acknowledgement via email including the bug report number
  414. within an hour; if you haven't received a confirmation, then the bug reporting process failed at some point (reportbug or MTA failure, BTS maintenance, etc.).\n""",
  415. (sysinfo['email'] % 'n'), (sysinfo['email'] % '999999'))
  416. # If we've stored more than one copy of the message, delete the
  417. # one without the SMTP headers.
  418. if filename and os.path.exists(msgname) and os.path.exists(filename):
  419. try:
  420. os.unlink(filename)
  421. except:
  422. pass
  423. if filename and os.path.exists(filename) and not mua:
  424. # Message is misleading if an MUA is used.
  425. ewrite("A copy of the report is stored as: %s\n" % filename)
  426. return