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.

mailmail.py 10KB

5 years ago
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402
  1. # -*- test-case-name: twisted.mail.test.test_mailmail -*-
  2. # Copyright (c) Twisted Matrix Laboratories.
  3. # See LICENSE for details.
  4. """
  5. Implementation module for the I{mailmail} command.
  6. """
  7. from __future__ import print_function
  8. import email.utils
  9. import os
  10. import sys
  11. import getpass
  12. try:
  13. # Python 3
  14. from configparser import ConfigParser
  15. except ImportError:
  16. # Python 2
  17. from ConfigParser import ConfigParser
  18. from twisted.copyright import version
  19. from twisted.internet import reactor
  20. from twisted.logger import Logger, textFileLogObserver
  21. from twisted.mail import smtp
  22. from twisted.python.compat import NativeStringIO
  23. GLOBAL_CFG = "/etc/mailmail"
  24. LOCAL_CFG = os.path.expanduser("~/.twisted/mailmail")
  25. SMARTHOST = '127.0.0.1'
  26. ERROR_FMT = """\
  27. Subject: Failed Message Delivery
  28. Message delivery failed. The following occurred:
  29. %s
  30. --
  31. The Twisted sendmail application.
  32. """
  33. _logObserver = textFileLogObserver(sys.stderr)
  34. _log = Logger(observer=_logObserver)
  35. class Options:
  36. """
  37. Store the values of the parsed command-line options to the I{mailmail}
  38. script.
  39. @type to: L{list} of L{str}
  40. @ivar to: The addresses to which to deliver this message.
  41. @type sender: L{str}
  42. @ivar sender: The address from which this message is being sent.
  43. @type body: C{file}
  44. @ivar body: The object from which the message is to be read.
  45. """
  46. def getlogin():
  47. try:
  48. return os.getlogin()
  49. except:
  50. return getpass.getuser()
  51. _unsupportedOption = SystemExit("Unsupported option.")
  52. def parseOptions(argv):
  53. o = Options()
  54. o.to = [e for e in argv if not e.startswith('-')]
  55. o.sender = getlogin()
  56. # Just be very stupid
  57. # Skip -bm -- it is the default
  58. # Add a non-standard option for querying the version of this tool.
  59. if '--version' in argv:
  60. print('mailmail version:', version)
  61. raise SystemExit()
  62. # -bp lists queue information. Screw that.
  63. if '-bp' in argv:
  64. raise _unsupportedOption
  65. # -bs makes sendmail use stdin/stdout as its transport. Screw that.
  66. if '-bs' in argv:
  67. raise _unsupportedOption
  68. # -F sets who the mail is from, but is overridable by the From header
  69. if '-F' in argv:
  70. o.sender = argv[argv.index('-F') + 1]
  71. o.to.remove(o.sender)
  72. # -i and -oi makes us ignore lone "."
  73. if ('-i' in argv) or ('-oi' in argv):
  74. raise _unsupportedOption
  75. # -odb is background delivery
  76. if '-odb' in argv:
  77. o.background = True
  78. else:
  79. o.background = False
  80. # -odf is foreground delivery
  81. if '-odf' in argv:
  82. o.background = False
  83. else:
  84. o.background = True
  85. # -oem and -em cause errors to be mailed back to the sender.
  86. # It is also the default.
  87. # -oep and -ep cause errors to be printed to stderr
  88. if ('-oep' in argv) or ('-ep' in argv):
  89. o.printErrors = True
  90. else:
  91. o.printErrors = False
  92. # -om causes a copy of the message to be sent to the sender if the sender
  93. # appears in an alias expansion. We do not support aliases.
  94. if '-om' in argv:
  95. raise _unsupportedOption
  96. # -t causes us to pick the recipients of the message from
  97. # the To, Cc, and Bcc headers, and to remove the Bcc header
  98. # if present.
  99. if '-t' in argv:
  100. o.recipientsFromHeaders = True
  101. o.excludeAddresses = o.to
  102. o.to = []
  103. else:
  104. o.recipientsFromHeaders = False
  105. o.exludeAddresses = []
  106. requiredHeaders = {
  107. 'from': [],
  108. 'to': [],
  109. 'cc': [],
  110. 'bcc': [],
  111. 'date': [],
  112. }
  113. buffer = NativeStringIO()
  114. while 1:
  115. write = 1
  116. line = sys.stdin.readline()
  117. if not line.strip():
  118. break
  119. hdrs = line.split(': ', 1)
  120. hdr = hdrs[0].lower()
  121. if o.recipientsFromHeaders and hdr in ('to', 'cc', 'bcc'):
  122. o.to.extend([
  123. email.utils.parseaddr(hdrs[1])[1]
  124. ])
  125. if hdr == 'bcc':
  126. write = 0
  127. elif hdr == 'from':
  128. o.sender = email.utils.parseaddr(hdrs[1])[1]
  129. if hdr in requiredHeaders:
  130. requiredHeaders[hdr].append(hdrs[1])
  131. if write:
  132. buffer.write(line)
  133. if not requiredHeaders['from']:
  134. buffer.write('From: {}\r\n'.format(o.sender))
  135. if not requiredHeaders['to']:
  136. if not o.to:
  137. raise SystemExit("No recipients specified.")
  138. buffer.write('To: {}\r\n'.format(', '.join(o.to)))
  139. if not requiredHeaders['date']:
  140. buffer.write('Date: {}\r\n'.format(smtp.rfc822date()))
  141. buffer.write(line)
  142. if o.recipientsFromHeaders:
  143. for a in o.excludeAddresses:
  144. try:
  145. o.to.remove(a)
  146. except:
  147. pass
  148. buffer.seek(0, 0)
  149. o.body = NativeStringIO(buffer.getvalue() + sys.stdin.read())
  150. return o
  151. class Configuration:
  152. """
  153. @ivar allowUIDs: A list of UIDs which are allowed to send mail.
  154. @ivar allowGIDs: A list of GIDs which are allowed to send mail.
  155. @ivar denyUIDs: A list of UIDs which are not allowed to send mail.
  156. @ivar denyGIDs: A list of GIDs which are not allowed to send mail.
  157. @type defaultAccess: L{bool}
  158. @ivar defaultAccess: L{True} if access will be allowed when no other access
  159. control rule matches or L{False} if it will be denied in that case.
  160. @ivar useraccess: Either C{'allow'} to check C{allowUID} first
  161. or C{'deny'} to check C{denyUID} first.
  162. @ivar groupaccess: Either C{'allow'} to check C{allowGID} first or
  163. C{'deny'} to check C{denyGID} first.
  164. @ivar identities: A L{dict} mapping hostnames to credentials to use when
  165. sending mail to that host.
  166. @ivar smarthost: L{None} or a hostname through which all outgoing mail will
  167. be sent.
  168. @ivar domain: L{None} or the hostname with which to identify ourselves when
  169. connecting to an MTA.
  170. """
  171. def __init__(self):
  172. self.allowUIDs = []
  173. self.denyUIDs = []
  174. self.allowGIDs = []
  175. self.denyGIDs = []
  176. self.useraccess = 'deny'
  177. self.groupaccess = 'deny'
  178. self.identities = {}
  179. self.smarthost = None
  180. self.domain = None
  181. self.defaultAccess = True
  182. def loadConfig(path):
  183. # [useraccess]
  184. # allow=uid1,uid2,...
  185. # deny=uid1,uid2,...
  186. # order=allow,deny
  187. # [groupaccess]
  188. # allow=gid1,gid2,...
  189. # deny=gid1,gid2,...
  190. # order=deny,allow
  191. # [identity]
  192. # host1=username:password
  193. # host2=username:password
  194. # [addresses]
  195. # smarthost=a.b.c.d
  196. # default_domain=x.y.z
  197. c = Configuration()
  198. if not os.access(path, os.R_OK):
  199. return c
  200. p = ConfigParser()
  201. p.read(path)
  202. au = c.allowUIDs
  203. du = c.denyUIDs
  204. ag = c.allowGIDs
  205. dg = c.denyGIDs
  206. for (section, a, d) in (('useraccess', au, du), ('groupaccess', ag, dg)):
  207. if p.has_section(section):
  208. for (mode, L) in (('allow', a), ('deny', d)):
  209. if p.has_option(section, mode) and p.get(section, mode):
  210. for sectionID in p.get(section, mode).split(','):
  211. try:
  212. sectionID = int(sectionID)
  213. except ValueError:
  214. _log.error(
  215. "Illegal {prefix}ID in "
  216. "[{section}] section: {sectionID}",
  217. prefix=section[0].upper(),
  218. section=section, sectionID=sectionID)
  219. else:
  220. L.append(sectionID)
  221. order = p.get(section, 'order')
  222. order = [s.split()
  223. for s in [s.lower()
  224. for s in order.split(',')]]
  225. if order[0] == 'allow':
  226. setattr(c, section, 'allow')
  227. else:
  228. setattr(c, section, 'deny')
  229. if p.has_section('identity'):
  230. for (host, up) in p.items('identity'):
  231. parts = up.split(':', 1)
  232. if len(parts) != 2:
  233. _log.error("Illegal entry in [identity] section: {section}",
  234. section=up)
  235. continue
  236. c.identities[host] = parts
  237. if p.has_section('addresses'):
  238. if p.has_option('addresses', 'smarthost'):
  239. c.smarthost = p.get('addresses', 'smarthost')
  240. if p.has_option('addresses', 'default_domain'):
  241. c.domain = p.get('addresses', 'default_domain')
  242. return c
  243. def success(result):
  244. reactor.stop()
  245. failed = None
  246. def failure(f):
  247. global failed
  248. reactor.stop()
  249. failed = f
  250. def sendmail(host, options, ident):
  251. d = smtp.sendmail(host, options.sender, options.to, options.body)
  252. d.addCallbacks(success, failure)
  253. reactor.run()
  254. def senderror(failure, options):
  255. recipient = [options.sender]
  256. sender = '"Internally Generated Message ({})"<postmaster@{}>'.format(
  257. sys.argv[0], smtp.DNSNAME.decode("ascii"))
  258. error = NativeStringIO()
  259. failure.printTraceback(file=error)
  260. body = NativeStringIO(ERROR_FMT % error.getvalue())
  261. d = smtp.sendmail('localhost', sender, recipient, body)
  262. d.addBoth(lambda _: reactor.stop())
  263. def deny(conf):
  264. uid = os.getuid()
  265. gid = os.getgid()
  266. if conf.useraccess == 'deny':
  267. if uid in conf.denyUIDs:
  268. return True
  269. if uid in conf.allowUIDs:
  270. return False
  271. else:
  272. if uid in conf.allowUIDs:
  273. return False
  274. if uid in conf.denyUIDs:
  275. return True
  276. if conf.groupaccess == 'deny':
  277. if gid in conf.denyGIDs:
  278. return True
  279. if gid in conf.allowGIDs:
  280. return False
  281. else:
  282. if gid in conf.allowGIDs:
  283. return False
  284. if gid in conf.denyGIDs:
  285. return True
  286. return not conf.defaultAccess
  287. def run():
  288. o = parseOptions(sys.argv[1:])
  289. gConf = loadConfig(GLOBAL_CFG)
  290. lConf = loadConfig(LOCAL_CFG)
  291. if deny(gConf) or deny(lConf):
  292. _log.error("Permission denied")
  293. return
  294. host = lConf.smarthost or gConf.smarthost or SMARTHOST
  295. ident = gConf.identities.copy()
  296. ident.update(lConf.identities)
  297. if lConf.domain:
  298. smtp.DNSNAME = lConf.domain
  299. elif gConf.domain:
  300. smtp.DNSNAME = gConf.domain
  301. sendmail(host, o, ident)
  302. if failed:
  303. if o.printErrors:
  304. failed.printTraceback(file=sys.stderr)
  305. raise SystemExit(1)
  306. else:
  307. senderror(failed, o)