mailmail.py 9.7 KB

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