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.
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
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
32 - thunderbird_cmd: must be a list of strings. default value is ["thunderbird"].
37 def read_config() -> dict:
38 config_files
= sorted((Path(xdg_config_home
) / "tails/automailer/").glob("*.toml"))
45 "Warning: could not import `toml`. Your configuration will be ignored",
51 for fpath
in config_files
:
52 with
open(fpath
) as fp
:
53 data
.update(toml
.load(fp
))
58 header
, body
= body
.split("\n\n", maxsplit
=1)
59 msg
= Parser(policy
=policy
.default
).parsestr(header
)
63 def get_attachments(msg
) -> list[str]:
64 attachments
: list[str] = []
67 for attachment_list
in msg
.get_all("x-attach"):
68 for fpath
in (f
.strip() for f
in attachment_list
.split(",")):
71 if not os
.path
.exists(fpath
):
72 print(f
"Skipping attachemt '{fpath}': not found", file=sys
.stderr
)
74 attachments
.append(fpath
)
79 def markdown_to_html(body
: str) -> str:
80 # Having import inside a function is a simple way of having optional dependencies
83 return markdown
.markdown(body
)
86 def mailer_thunderbird(body
: str):
87 msg
, body
= parse(body
)
89 html
= msg
.get("Content-Type", "text/plain") == "text/html"
90 for key
in ["to", "cc", "subject"]:
92 spec
.append(f
"{key}='{msg[key]}'")
93 attachments
= get_attachments(msg
)
95 spec
.append("attachment='%s'" % ",".join(attachments
))
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
:
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
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"]:
121 cmdline
.append(f
"--{key}={msg[key]}")
122 attachments
= get_attachments(msg
)
127 f
'<#part filename="{attachment}" disposition=attachment><#/part>'
128 for attachment
in attachments
135 with tempfile
.TemporaryDirectory() as tmpdir
:
136 fpath
= Path(tmpdir
) / "email.eml"
137 with fpath
.open("w") as fp
:
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":
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(
159 default
=config
.get("mailer", "print"),
160 choices
=["print", "thunderbird", "notmuch"],
161 help="Your favorite MUA",
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
)
173 if __name__
== "__main__":
174 parser
= get_parser()
175 args
= parser
.parse_args()
179 body
= sys
.stdin
.read()
180 mailer(args
.mailer
, body
)