mutt's secret sauce
text/html ; uconv --add-signature -f %{charset} -t UTF-8 %s | sponge %s && firefox -new-tab %s & sleep 5 ; description="HTML Document" ; test=test -n "$DISPLAY" ; nametemplate=%s.html | |
text/html ; w3m -I %{charset} -T text/html %s ; description="HTML Document" ; nametemplate=%s.html ; copiousoutput | |
text/* ; nvim -Rc Goyo '%s' ; edit=nvim -c Goyo '%s' ; compose=nvim -c Goyo '%s' ; needsterminal | |
application/pdf ; zathura %s &> /dev/null ; description="PDF Document" ; test=test -n "$DISPLAY" ; nametemplate=%s.pdf | |
application/pdf ; pdftotext %s - ; description="PDF Document" ; nametemplate=%s.pdf ; copiousoutput | |
image/pdf ; imv %s &> /dev/null ; description="PDF Image" ; test=test "$DISPLAY" ; nametemplate=%s.pdf | |
image/gif ; imv %s &> /dev/null ; description="GIF Image" ; test=test "$DISPLAY" ; nametemplate=%s.gif | |
image/jpeg ; imv %s &> /dev/null ; description="JPEG Image" ; test=test "$DISPLAY" ; nametemplate=%s.jpeg | |
image/pjpeg ; imv %s &> /dev/null ; description="JPEG Image" ; test=test "$DISPLAY" ; nametemplate=%s.jpeg | |
image/tiff ; imv %s &> /dev/null ; description="TIFF Image" ; test=test "$DISPLAY" ; nametemplate=%s.tiff | |
image/x-portable-bitmap ; imv %s &> /dev/null ; description="PBM Image" ; test=test "$DISPLAY" ; nametemplate=%s.pbm | |
image/x-portable-graymap ; imv %s &> /dev/null ; description="PGM Image" ; test=test "$DISPLAY" ; nametemplate=%s.pgm | |
image/x-portable-pixmap ; imv %s &> /dev/null ; description="PPM Image" ; test=test "$DISPLAY" ; nametemplate=%s.ppm | |
image/x-xbitmap ; imv %s &> /dev/null ; description="XBM Image" ; test=test "$DISPLAY" ; nametemplate=%s.xbm | |
image/x-xpixmap ; imv %s &> /dev/null ; description="XPM Image" ; test=test "$DISPLAY" ; nametemplate=%s.xpm | |
image/bmp ; imv %s &> /dev/null ; description="BMP Image" ; test=test "$DISPLAY" ; nametemplate=%s.bmp | |
image/x-bmp ; imv %s &> /dev/null ; description="BMP Image" ; test=test "$DISPLAY" ; nametemplate=%s.bmp | |
image/x-ms-bmp ; imv %s &> /dev/null ; description="BMP Image" ; test=test "$DISPLAY" ; nametemplate=%s.bmp | |
image/x-rgb ; imv %s &> /dev/null ; description="RGB Image" ; test=test "$DISPLAY" ; nametemplate=%s.rgb | |
image/targa ; imv %s &> /dev/null ; description="TARGA Image" ; test=test "$DISPLAY" ; nametemplate=%s.tga | |
image/fits ; imv %s &> /dev/null ; description="FITS Image" ; test=test "$DISPLAY" ; nametemplate=%s.fits | |
image/png ; imv %s &> /dev/null ; description="PNG Image" ; test=test "$DISPLAY" ; nametemplate=%s.png | |
image/pm ; imv %s &> /dev/null ; description="PM Image" ; test=test "$DISPLAY" ; nametemplate=%s.pm |
<!DOCTYPE html> | |
<html> | |
<head> | |
<meta charset="utf-8"> | |
<meta name="ProgId" content="Word.Document"> | |
<meta name="viewport" content="width=device-width, initial-scale=1"> | |
<meta http-equiv="X-UA-Compatible" content="IE=edge" /> | |
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" /> | |
$for(author-meta)$ | |
<meta name="author" content="$author-meta$" /> | |
$endfor$ | |
$if(date-meta)$ | |
<meta name="date" content="$date-meta$" /> | |
$endif$ | |
$if(keywords)$ | |
<meta name="keywords" content="$for(keywords)$$keywords$$sep$, $endfor$" /> | |
$endif$ | |
<title>$if(title-prefix)$$title-prefix$ – $endif$$pagetitle$</title> | |
<style type="text/css"> | |
body, table, td, a { | |
-webkit-text-size-adjust: 100%; | |
-ms-text-size-adjust: 100%; | |
-webkit-font-smoothing: antialiased; | |
} | |
table, td { mso-table-lspace: 0pt; mso-table-rspace: 0pt; } | |
img { | |
border: 0; | |
height: auto; | |
line-height: 100%; | |
outline: none; | |
text-decoration: none; | |
display: inline-block; | |
padding-top: 7px; | |
max-width: 100%; | |
-ms-interpolation-mode: bicubic; | |
} | |
figure { margin: 0; padding: 0; } | |
article, aside, details, figcaption, figure,footer, header, hgroup, menu, nav, section { display: block; } | |
a[x-apple-data-detectors] { | |
color: inherit !important; | |
text-decoration: none !important; | |
font-size: inherit !important; | |
font-family: inherit !important; | |
font-weight: inherit !important; | |
line-height: inherit !important; | |
} | |
div[style*="margin: 16px 0;"] { margin: 0 !important; } | |
table, td, div, a { box-sizing: border-box; } | |
body, .body { | |
font-family: Georgia, "Times New Roman", Times, serif; | |
font-size: 14px; | |
line-height: 1.2; | |
height: 100% !important; | |
width: 100% !important; | |
margin: 0 !important; | |
padding: 0 !important; | |
} | |
table { border-collapse: separate !important; width: 100%; } | |
table td { font-family: Georgia, "Times New Roman", Times, serif; font-size: 14px; vertical-align: top; } | |
strong, b { font-weight: bold; } | |
h1, h2, h3, h4, h5, h6 { | |
color: #222222 !important; | |
font-family: "Roboto", "Helvetica Neue", "Segoe UI", Helvetica, Arial, sans-serif; | |
font-weight: bold; | |
line-height: 1.2; | |
margin: 0; | |
margin-bottom: 7px; | |
margin-top: 10.5px; | |
} | |
h1, h2 { margin-bottom: 10.5px; margin-top: 14px; } | |
h1 { font-size: 22.4px; text-transform: capitalize; } | |
h2 { font-size: 19.6px; } | |
h3 { font-size: 16.8px; } | |
h4 { font-size: 15.4px; } | |
h5 { font-size: 14px; } | |
h6 { font-size: 12.6px; } | |
p, ul, ol { | |
font-family: Georgia, "Times New Roman", Times, serif; | |
font-size: 14px; | |
font-weight: normal; | |
margin: 0; | |
padding-top: 7px; | |
color: #111111; | |
} | |
ul, ol { margin: 0; margin-bottom: 7.5px; padding-left: 25px; } | |
ul li, ol li { list-style-position: outside; margin-left: 5px; margin-bottom: 1px; } | |
li > ul, li > ol { margin-top: 7.5px; } | |
a { color: #348eda; text-decoration: none; font-weight: bold; } | |
pre > a, code > a { color: none; font-weight: normal; } | |
code, pre { | |
word-break: break-word; | |
word-wrap: break-word; | |
-webkit-hyphens: auto; | |
-moz-hyphens: auto; | |
hyphens: auto; | |
font-family: Menlo, Monaco, Consolas, "Courier New", monospace; | |
font-size: 11.9px; | |
} | |
pre { | |
display: block; | |
width: 96%; | |
margin: 1em 0; | |
margin-bottom: 9px; | |
background: #f8f8f8; | |
padding: 1%; | |
white-space: pre-wrap; | |
border-radius: 4px; | |
} | |
p > code { color: #111111; background: none; } | |
blockquote { | |
padding: 0 7px 0 7px; | |
border-left: 2px solid #cccccc; | |
border-top: 4.2px solid transparent; | |
font-style: italic; | |
margin: 0 0 7px 3px; | |
} | |
blockquote p { padding: 0; } | |
mark { background: #ff0; } | |
dl dt { font-weight: bold; } | |
dl dd { margin-left: 28px; } | |
hr { margin: 28px 0; border: none; border-top: 1px solid #cccccc; } | |
@media only screen and (max-width: 840px) { | |
table[class=body] { font-size: 12px !important; } | |
table[class=body] p, table[class=body] ul, table[class=body] ol { font-size: 12px !important; } | |
table[class=body] h1 { font-size: 18.2px !important; } | |
table[class=body] h2 { font-size: 16.8px !important; } | |
table[class=body] h3 { font-size: 15.4px !important; } | |
table[class=body] h4 { font-size: 14.7px !important; } | |
table[class=body] h5 { font-size: 12.6px !important; } | |
table[class=body] h6 { font-size: 11.9px !important; } | |
table[class=body] h1, table[class=body] h2 { margin-bottom: 14px !important; margin-top: 14px !important; } | |
} | |
</style> | |
$if(quotes)$ | |
<style type="text/css">q { quotes: "“" "”" "‘" "’"; }</style> | |
$endif$ | |
$if(highlighting-css)$ | |
<style type="text/css"> | |
$highlighting-css$ | |
</style> | |
$endif$ | |
$for(css)$ | |
<link rel="stylesheet" href="$css$" type="text/css" /> | |
$endfor$ | |
$if(math)$ | |
$math$ | |
$endif$ | |
$for(header-includes)$ | |
$header-includes$ | |
$endfor$ | |
</head> | |
<body> | |
$for(include-before)$ | |
$include-before$ | |
$endfor$ | |
$if(title)$ | |
<div id="$idprefix$header"> | |
<h1 class="title">$title$</h1> | |
$if(subtitle)$ | |
<h1 class="subtitle">$subtitle$</h1> | |
$endif$ | |
$for(author)$ | |
<h2 class="author">$author$</h2> | |
$endfor$ | |
$if(date)$ | |
<h3 class="date">$date$</h3> | |
$endif$ | |
</div> | |
$endif$ | |
$if(toc)$ | |
<div id="$idprefix$TOC"> | |
$toc$ | |
</div> | |
$endif$ | |
$body$ | |
$for(include-after)$ | |
$include-after$ | |
$endfor$ | |
</body> | |
</html> |
#!/usr/bin/env python3 | |
import re | |
import sys | |
import email | |
import shlex | |
import mimetypes | |
import subprocess | |
from copy import copy | |
from hashlib import md5 | |
from email import charset | |
from email import encoders | |
from email.mime.text import MIMEText | |
from email.mime.multipart import MIMEMultipart | |
from email.mime.nonmultipart import MIMENonMultipart | |
from os.path import basename, splitext, expanduser | |
charset.add_charset('utf-8', charset.SHORTEST, '8bit') | |
def pandoc(from_format, to_format='markdown', plain='markdown', title=None): | |
markdown = ('markdown' | |
'-blank_before_blockquote') | |
if from_format == 'plain': | |
from_format = plain | |
if from_format == 'markdown': | |
from_format = markdown | |
if to_format == 'markdown': | |
to_format = markdown | |
command = 'pandoc -f {} -t {} --standalone --highlight-style=tango' | |
if to_format in ('html', 'html5'): | |
if title is not None: | |
command += ' --variable=pagetitle:{}'.format(shlex.quote(title)) | |
command += ' --webtex --template={}'.format( | |
expanduser('~/.pandoc/templates/email.html')) | |
return command.format(from_format, to_format) | |
def gmailfy(payload): | |
return payload.replace('<blockquote>', | |
'<blockquote class="gmail_quote" style="' | |
'padding: 0 7px 0 7px;' | |
'border-left: 2px solid #cccccc;' | |
'font-style: italic;' | |
'margin: 0 0 7px 3px;' | |
'">') | |
def make_alternative(message, part): | |
alternative = convert(part, 'html', | |
pandoc(part.get_content_subtype(), | |
to_format='html', | |
title=message.get('Subject'))) | |
alternative.set_payload(gmailfy(alternative.get_payload())) | |
return alternative | |
def make_replacement(message, part): | |
return convert(part, 'plain', pandoc(part.get_content_subtype())) | |
def convert(part, to_subtype, command): | |
payload = part.get_payload() | |
if isinstance(payload, str): | |
payload = payload.encode('utf-8') | |
else: | |
payload = part.get_payload(None, True) | |
if not isinstance(payload, bytes): | |
payload = payload.encode('utf-8') | |
process = subprocess.run( | |
shlex.split(command), | |
input=payload, stdout=subprocess.PIPE, check=True) | |
return MIMEText(process.stdout, to_subtype, 'utf-8') | |
def with_alternative(parent, part, from_signed, | |
make_alternative=make_alternative, | |
make_replacement=None): | |
try: | |
alternative = make_alternative(parent or part, from_signed or part) | |
replacement = (make_replacement(parent or part, part) | |
if from_signed is None and make_replacement is not None | |
else part) | |
except: | |
return parent or part | |
envelope = MIMEMultipart('alternative') | |
if parent is None: | |
for k, v in part.items(): | |
if (k.lower() != 'mime-version' | |
and not k.lower().startswith('content-')): | |
envelope.add_header(k, v) | |
del part[k] | |
envelope.attach(replacement) | |
envelope.attach(alternative) | |
if parent is None: | |
return envelope | |
payload = parent.get_payload() | |
payload[payload.index(part)] = envelope | |
return parent | |
def tag_attachments(message): | |
if message.get_content_type() == 'multipart/mixed': | |
for part in message.get_payload(): | |
if (part.get_content_maintype() in ['image'] | |
and 'Content-ID' not in part): | |
filename = part.get_param('filename', | |
header='Content-Disposition') | |
if isinstance(filename, tuple): | |
filename = str(filename[2], filename[0] or 'us-ascii') | |
if filename: | |
filename = splitext(basename(filename))[0] | |
if filename: | |
part.add_header('Content-ID', '<{}>'.format(filename)) | |
return message | |
def attachment_from_file_path(attachment_path): | |
try: | |
mime, encoding = mimetypes.guess_type(attachment_path, strict=False) | |
maintype, subtype = mime.split('/') | |
with open(attachment_path, 'rb') as payload: | |
attachment = MIMENonMultipart(maintype, subtype) | |
attachment.set_payload(payload.read()) | |
encoders.encode_base64(attachment) | |
if encoding: | |
attachment.add_header('Content-Encoding', encoding) | |
return attachment | |
except: | |
return None | |
attachment_path_pattern = re.compile(r'\]\s*\(\s*file://(/[^)]*\S)\s*\)|' | |
r'\]\s*:\s*file://(/.*\S)\s*$', | |
re.MULTILINE) | |
def link_attachments(payload): | |
attached = [] | |
attachments = [] | |
def on_match(match): | |
if match.group(1): | |
attachment_path = match.group(1) | |
cid_fmt = '](cid:{})' | |
else: | |
attachment_path = match.group(2) | |
cid_fmt = ']: cid:{}' | |
attachment_id = md5(attachment_path.encode()).hexdigest() | |
if attachment_id in attached: | |
return cid_fmt.format(attachment_id) | |
attachment = attachment_from_file_path(attachment_path) | |
if attachment: | |
attachment.add_header('Content-ID', '<{}>'.format(attachment_id)) | |
attachments.append(attachment) | |
attached.append(attachment_id) | |
return cid_fmt.format(attachment_id) | |
return match.group() | |
return attachments, attachment_path_pattern.sub(on_match, payload) | |
def with_local_attachments(parent, part, from_signed, | |
link_attachments=link_attachments): | |
if from_signed is None: | |
attachments, payload = link_attachments(part.get_payload()) | |
part.set_payload(payload) | |
else: | |
attachments, payload = link_attachments(from_signed.get_payload()) | |
from_signed = copy(from_signed) | |
from_signed.set_payload(payload) | |
if not attachments: | |
return parent, part, from_signed | |
if parent is None: | |
parent = MIMEMultipart('mixed') | |
for k, v in part.items(): | |
if (k.lower() != 'mime-version' | |
and not k.lower().startswith('content-')): | |
parent.add_header(k, v) | |
del part[k] | |
parent.attach(part) | |
for attachment in attachments: | |
parent.attach(attachment) | |
return parent, part, from_signed | |
def is_target(part, target_subtypes): | |
return (part.get('Content-Disposition', 'inline') == 'inline' | |
and part.get_content_maintype() == 'text' | |
and part.get_content_subtype() in target_subtypes) | |
def pick_from_signed(part, target_subtypes): | |
for from_signed in part.get_payload(): | |
if is_target(from_signed, target_subtypes): | |
return from_signed | |
def seek_target(message, target_subtypes=['plain', 'markdown']): | |
if message.is_multipart(): | |
if message.get_content_type() == 'multipart/signed': | |
part = pick_from_signed(message, target_subtypes) | |
if part is not None: | |
return None, message, part | |
elif message.get_content_type() == 'multipart/mixed': | |
for part in message.get_payload(): | |
if part.is_multipart(): | |
if part.get_content_type() == 'multipart/signed': | |
from_signed = pick_from_signed(part, target_subtypes) | |
if from_signed is not None: | |
return message, part, from_signed | |
elif is_target(part, target_subtypes): | |
return message, part, None | |
else: | |
if is_target(message, target_subtypes): | |
return None, message, None | |
return None, None, None | |
def main(): | |
try: | |
message = email.message_from_file(sys.stdin) | |
parent, part, from_signed = seek_target(message) | |
if (parent, part, from_signed) == (None, None, None): | |
print(message) | |
return | |
tag_attachments(message) | |
print(with_alternative( | |
*with_local_attachments(parent, part, from_signed))) | |
except (BrokenPipeError, KeyboardInterrupt): | |
pass | |
if __name__ == '__main__': | |
main() |
#!/usr/bin/env python3 | |
import re | |
import sys | |
import email | |
import shlex | |
import mimetypes | |
import subprocess | |
from copy import copy | |
from hashlib import md5 | |
from email import charset | |
from email import encoders | |
from email.mime.text import MIMEText | |
from email.mime.multipart import MIMEMultipart | |
from email.mime.nonmultipart import MIMENonMultipart | |
from os.path import basename, splitext, expanduser | |
charset.add_charset('utf-8', charset.SHORTEST, '8bit') | |
def pandoc(from_format, to_format='markdown', plain='markdown', title=None): | |
markdown = ('markdown' | |
'-blank_before_blockquote') | |
if from_format == 'plain': | |
from_format = plain | |
if from_format == 'markdown': | |
from_format = markdown | |
if to_format == 'markdown': | |
to_format = markdown | |
command = 'pandoc -f {} -t {} --standalone --highlight-style=tango' | |
if to_format in ('html', 'html5'): | |
if title is not None: | |
command += ' --variable=pagetitle:{}'.format(shlex.quote(title)) | |
command += ' --webtex --template={}'.format( | |
expanduser('~/.pandoc/templates/email.html')) | |
return command.format(from_format, to_format) | |
def gmailfy(payload): | |
return payload.replace('<blockquote>', | |
'<blockquote class="gmail_quote" style="' | |
'padding: 0 7px 0 7px;' | |
'border-left: 2px solid #cccccc;' | |
'font-style: italic;' | |
'margin: 0 0 7px 3px;' | |
'">') | |
def make_alternative(message, part): | |
alternative = convert(part, 'html', | |
pandoc(part.get_content_subtype(), | |
to_format='html', | |
title=message.get('Subject'))) | |
alternative.set_payload(gmailfy(alternative.get_payload())) | |
return alternative | |
def make_replacement(message, part): | |
return convert(part, 'plain', pandoc(part.get_content_subtype())) | |
def convert(part, to_subtype, command): | |
payload = part.get_payload(decode=True) | |
process = subprocess.run( | |
shlex.split(command), | |
input=payload, stdout=subprocess.PIPE, check=True) | |
return MIMEText(process.stdout, to_subtype, 'utf-8') | |
def with_alternative(parent, part, from_signed, | |
make_alternative=make_alternative, | |
make_replacement=None): | |
try: | |
alternative = make_alternative(parent or part, from_signed or part) | |
replacement = (make_replacement(parent or part, part) | |
if from_signed is None and make_replacement is not None | |
else part) | |
except: | |
return parent or part | |
envelope = MIMEMultipart('alternative') | |
if parent is None: | |
for k, v in part.items(): | |
if (k.lower() != 'mime-version' | |
and not k.lower().startswith('content-')): | |
envelope.add_header(k, v) | |
del part[k] | |
envelope.attach(replacement) | |
envelope.attach(alternative) | |
if parent is None: | |
return envelope | |
payload = parent.get_payload() | |
payload[payload.index(part)] = envelope | |
return parent | |
def tag_attachments(message): | |
if message.get_content_type() == 'multipart/mixed': | |
for part in message.get_payload(): | |
if (part.get_content_maintype() in ['image'] | |
and 'Content-ID' not in part): | |
filename = part.get_param('filename', | |
header='Content-Disposition') | |
if isinstance(filename, tuple): | |
filename = str(filename[2], filename[0] or 'us-ascii') | |
if filename: | |
filename = splitext(basename(filename))[0] | |
if filename: | |
part.add_header('Content-ID', '<{}>'.format(filename)) | |
return message | |
def attachment_from_file_path(attachment_path): | |
try: | |
mime, encoding = mimetypes.guess_type(attachment_path, strict=False) | |
maintype, subtype = mime.split('/') | |
with open(attachment_path, 'rb') as payload: | |
attachment = MIMENonMultipart(maintype, subtype) | |
attachment.set_payload(payload.read()) | |
encoders.encode_base64(attachment) | |
if encoding: | |
attachment.add_header('Content-Encoding', encoding) | |
return attachment | |
except: | |
return None | |
attachment_path_pattern = re.compile(r'\]\s*\(\s*file://(/[^)]*\S)\s*\)|' | |
r'\]\s*:\s*file://(/.*\S)\s*$', | |
re.MULTILINE) | |
def link_attachments(payload): | |
attached = [] | |
attachments = [] | |
def on_match(match): | |
if match.group(1): | |
attachment_path = match.group(1) | |
cid_fmt = '](cid:{})' | |
else: | |
attachment_path = match.group(2) | |
cid_fmt = ']: cid:{}' | |
attachment_id = md5(attachment_path.encode()).hexdigest() | |
if attachment_id in attached: | |
return cid_fmt.format(attachment_id) | |
attachment = attachment_from_file_path(attachment_path) | |
if attachment: | |
attachment.add_header('Content-ID', '<{}>'.format(attachment_id)) | |
attachments.append(attachment) | |
attached.append(attachment_id) | |
return cid_fmt.format(attachment_id) | |
return match.group() | |
return attachments, attachment_path_pattern.sub(on_match, payload) | |
def with_local_attachments(parent, part, from_signed, | |
link_attachments=link_attachments): | |
if from_signed is None: | |
attachments, payload = link_attachments(part.get_payload()) | |
part.set_payload(payload) | |
else: | |
attachments, payload = link_attachments(from_signed.get_payload()) | |
from_signed = copy(from_signed) | |
from_signed.set_payload(payload) | |
if not attachments: | |
return parent, part, from_signed | |
if parent is None: | |
parent = MIMEMultipart('mixed') | |
for k, v in part.items(): | |
if (k.lower() != 'mime-version' | |
and not k.lower().startswith('content-')): | |
parent.add_header(k, v) | |
del part[k] | |
parent.attach(part) | |
for attachment in attachments: | |
parent.attach(attachment) | |
return parent, part, from_signed | |
def is_target(part, target_subtypes): | |
return (part.get('Content-Disposition', 'inline') == 'inline' | |
and part.get_content_maintype() == 'text' | |
and part.get_content_subtype() in target_subtypes) | |
def pick_from_signed(part, target_subtypes): | |
for from_signed in part.get_payload(): | |
if is_target(from_signed, target_subtypes): | |
return from_signed | |
def seek_target(message, target_subtypes=['plain', 'markdown']): | |
if message.is_multipart(): | |
if message.get_content_type() == 'multipart/signed': | |
part = pick_from_signed(message, target_subtypes) | |
if part is not None: | |
return None, message, part | |
elif message.get_content_type() == 'multipart/mixed': | |
for part in message.get_payload(): | |
if part.is_multipart(): | |
if part.get_content_type() == 'multipart/signed': | |
from_signed = pick_from_signed(part, target_subtypes) | |
if from_signed is not None: | |
return message, part, from_signed | |
elif is_target(part, target_subtypes): | |
return message, part, None | |
else: | |
if is_target(message, target_subtypes): | |
return None, message, None | |
return None, None, None | |
def main(): | |
try: | |
message = email.message_from_file(sys.stdin) | |
parent, part, from_signed = seek_target(message) | |
if (parent, part, from_signed) == (None, None, None): | |
print(message) | |
return | |
tag_attachments(message) | |
print(with_alternative( | |
*with_local_attachments(parent, part, from_signed))) | |
except (BrokenPipeError, KeyboardInterrupt): | |
pass | |
if __name__ == '__main__': | |
main() |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment