Funktionierender Prototyp des Serious Games zur Vermittlung von Wissen zu Software-Engineering-Arbeitsmodellen.
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

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