Merge branch 'stable' into devel
[tails.git] / bin / automailer.py
blob43b6f0fd2a7fe1092ac955ba7917b97d0ad60fe7
1 #!/usr/bin/python3
2 """
3 Standalone/module to simplify composing emails.
5 The purpose of this module is to make it easier to convert machine-ready
6 emails into actually composing emails.
7 """
9 import os.path
10 import subprocess
11 import sys
12 import tempfile
13 import time
14 from argparse import ArgumentDefaultsHelpFormatter, ArgumentParser
15 from email import policy
16 from email.parser import Parser
17 from functools import lru_cache
18 from pathlib import Path
20 from xdg.BaseDirectory import xdg_config_home
22 LONG_HELP = """
23 ## Configuration
25 To configure automailer, put config files in ~/.config/tails/automailer/*.toml
27 Supported options are:
28 - mailer: must be a string. supported values are
29 - "print"
30 - "thunderbird"
31 - "notmuch"
32 - thunderbird_cmd: must be a list of strings. default value is ["thunderbird"].
33 """
36 @lru_cache(maxsize=1)
37 def read_config() -> dict:
38 config_files = sorted((Path(xdg_config_home) / "tails/automailer/").glob("*.toml"))
39 if not config_files:
40 return {}
41 try:
42 import toml
43 except ImportError:
44 print(
45 "Warning: could not import `toml`. Your configuration will be ignored",
46 file=sys.stderr,
48 return {}
50 data = {}
51 for fpath in config_files:
52 with open(fpath) as fp:
53 data.update(toml.load(fp))
54 return data
57 def parse(body: str):
58 header, body = body.split("\n\n", maxsplit=1)
59 msg = Parser(policy=policy.default).parsestr(header)
60 return msg, body
63 def get_attachments(msg) -> list[str]:
64 attachments: list[str] = []
66 if "x-attach" in msg:
67 for attachment_list in msg.get_all("x-attach"):
68 for fpath in (f.strip() for f in attachment_list.split(",")):
69 if not fpath:
70 continue
71 if not os.path.exists(fpath):
72 print(f"Skipping attachemt '{fpath}': not found", file=sys.stderr)
73 continue
74 attachments.append(fpath)
76 return attachments
79 def markdown_to_html(body: str) -> str:
80 # Having import inside a function is a simple way of having optional dependencies
81 import markdown
83 return markdown.markdown(body)
86 def mailer_thunderbird(body: str):
87 msg, body = parse(body)
88 spec = []
89 html = msg.get("Content-Type", "text/plain") == "text/html"
90 for key in ["to", "cc", "subject"]:
91 if key in msg:
92 spec.append(f"{key}='{msg[key]}'")
93 attachments = get_attachments(msg)
94 if attachments:
95 spec.append("attachment='%s'" % ",".join(attachments))
97 if html:
98 body = markdown_to_html(body)
100 thunderbird_cmd = read_config().get("thunderbird_cmd", ["thunderbird"])
101 with tempfile.TemporaryDirectory() as tmpdir:
102 fpath = Path(tmpdir) / "email.eml"
103 with fpath.open("w") as fp:
104 fp.write(body)
105 spec.append("format=%s" % ("html" if html else "text"))
106 spec.append(f"message={fpath}")
107 cmdline = [*thunderbird_cmd, "-compose", ",".join(spec)]
108 subprocess.check_output(cmdline) # noqa: S603
110 # this is a workaround to the fact that Thunderbird will terminate *before* reading the file
111 # we don't really know how long does it take, but let's assume 2s are enough
112 time.sleep(2)
115 def mailer_notmuch(body: str):
116 msg, body = parse(body)
117 cmdline = ["notmuch-emacs-mua", "--client", "--create-frame"]
119 for key in ["to", "cc", "subject"]:
120 if key in msg:
121 cmdline.append(f"--{key}={msg[key]}")
122 attachments = get_attachments(msg)
123 if attachments:
124 body = (
125 "\n".join(
127 f'<#part filename="{attachment}" disposition=attachment><#/part>'
128 for attachment in attachments
131 + "\n\n"
132 + body
135 with tempfile.TemporaryDirectory() as tmpdir:
136 fpath = Path(tmpdir) / "email.eml"
137 with fpath.open("w") as fp:
138 fp.write(body)
139 cmdline.append(f"--body={fpath}")
141 subprocess.check_output(cmdline) # noqa: S603
144 def mailer(mailer: str, body: str):
145 if mailer == "thunderbird":
146 return mailer_thunderbird(body)
147 if mailer == "notmuch":
148 return mailer_notmuch(body)
149 if not mailer or mailer == "print":
150 print(body)
151 else:
152 print(f"Unsupported mailer: '{mailer}'")
155 def add_parser_mailer(parser: ArgumentParser, config: dict):
156 mail_options = parser.add_argument_group("mail options")
157 mail_options.add_argument(
158 "--mailer",
159 default=config.get("mailer", "print"),
160 choices=["print", "thunderbird", "notmuch"],
161 help="Your favorite MUA",
165 def get_parser():
166 config = read_config()
167 argparser = ArgumentParser(formatter_class=ArgumentDefaultsHelpFormatter)
168 argparser.add_argument("--long-help", action="store_true", default=False)
169 add_parser_mailer(argparser, config)
170 return argparser
173 if __name__ == "__main__":
174 parser = get_parser()
175 args = parser.parse_args()
176 if args.long_help:
177 print(LONG_HELP)
178 sys.exit(0)
179 body = sys.stdin.read()
180 mailer(args.mailer, body)