Skip to content

Commit 41a4ab2

Browse files
committed
sending quarto output as email
1 parent e6db181 commit 41a4ab2

File tree

4 files changed

+188
-0
lines changed

4 files changed

+188
-0
lines changed

examples/quarto_email.py

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
## TODO: work in progress
2+
3+
from dotenv import load_dotenv
4+
import os
5+
import base64
6+
import json
7+
8+
from email.mime.multipart import MIMEMultipart
9+
from email.mime.text import MIMEText
10+
from email.mime.image import MIMEImage
11+
import smtplib
12+
13+
14+
load_dotenv()
15+
16+
password = os.environ["GMAIL_APP_PASSWORD"]
17+
username = "jules.walzergoldfeld@gmail.com"
18+
19+
with open(".output_metadata.json", "r", encoding="utf-8") as f:
20+
metadata = json.load(f)
21+
22+
# Get email content (if present)
23+
email_content = metadata.get("rsc_email_body_html", "")
24+
25+
# Get email images (dictionary: {filename: base64_string})
26+
email_images = metadata.get("rsc_email_images", {})
27+
28+
# Compose the email
29+
msg = MIMEMultipart("related")
30+
msg["Subject"] = "hello world"
31+
msg["From"] = username
32+
msg["To"] = username
33+
34+
msg_alt = MIMEMultipart("alternative")
35+
msg.attach(msg_alt)
36+
msg_alt.attach(MIMEText(email_content, "html"))
37+
38+
# Attach images
39+
for image_name, image_base64 in email_images.items():
40+
img_bytes = base64.b64decode(image_base64)
41+
img = MIMEImage(img_bytes, _subtype="png")
42+
img.add_header('Content-ID', f'<{image_name}>')
43+
msg.attach(img)
44+
45+
with smtplib.SMTP_SSL("smtp.gmail.com", 465) as server:
46+
server.login(username, password)
47+
server.sendmail(msg["From"], [msg["To"]], msg.as_string())

send-quarto-emails/.output_metadata.json

Lines changed: 1 addition & 0 deletions
Large diffs are not rendered by default.

send-quarto-emails/main.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
from methods import send_quarto_email_with_gmail
2+
import os
3+
from dotenv import load_dotenv
4+
5+
os.chdir(os.path.dirname(os.path.abspath(__file__)))
6+
7+
load_dotenv()
8+
9+
password = os.environ["GMAIL_APP_PASSWORD"]
10+
username = os.environ["GMAIL_ADDRESS"]
11+
12+
send_quarto_email_with_gmail(
13+
username=username,
14+
password=password,
15+
json_path=".output_metadata.json",
16+
recipients=[username, "jules.walzergoldfeld@posit.co"]
17+
)

send-quarto-emails/methods.py

Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
## TODO: work in progress
2+
3+
from dataclasses import dataclass
4+
import base64
5+
import json
6+
7+
from email.mime.multipart import MIMEMultipart
8+
from email.mime.text import MIMEText
9+
from email.mime.image import MIMEImage
10+
import smtplib
11+
12+
13+
@dataclass
14+
class IntermediateDataStruct:
15+
html: str
16+
images: dict[str, str]
17+
subject: str
18+
rsc_email_supress_report_attachment: bool
19+
rsc_email_supress_scheduled: bool
20+
21+
text: str = None # not present in quarto
22+
recipients: list[str] = None # not present in quarto
23+
24+
25+
def redmail_to_intermediate_struct():
26+
## This will require 2 steps:
27+
# 1. jinja to html
28+
# 2. pulling attachments out
29+
pass
30+
31+
32+
def yagmail_to_intermediate_struct():
33+
pass
34+
35+
36+
def mjml_to_intermediate_struct():
37+
## This will require 2 steps:
38+
# 1. mjml2html
39+
# 2. pulling attachments out
40+
pass
41+
42+
43+
# Some Connect handling happens here: https://github.com/posit-dev/connect/blob/c84f845f9e75887f6450b32f1071e57e8777b8b1/src/connect/reports/output_metadata.go
44+
45+
def _read_quarto_email_json(path: str) -> IntermediateDataStruct:
46+
with open(path, "r", encoding="utf-8") as f:
47+
metadata = json.load(f)
48+
49+
email_html = metadata.get("rsc_email_body_html", "")
50+
email_subject = metadata.get("rsc_email_subject", "")
51+
email_text = metadata.get("rsc_email_body_text", "")
52+
53+
# Other metadata fields, as per https://github.com/posit-dev/connect/wiki/Rendering#output-metadata-fields-and-validation
54+
# These might be rmd specific, not quarto
55+
# metadata.get("rsc_output_files", [])
56+
# metadata.get("rsc_email_attachments", [])
57+
58+
# Get email images (dictionary: {filename: base64_string})
59+
email_images = metadata.get("rsc_email_images", {})
60+
61+
supress_report_attachment = metadata.get(
62+
"rsc_email_supress_report_attachment", False
63+
)
64+
supress_scheduled = metadata.get("rsc_email_supress_scheduled", False)
65+
66+
iStruct = IntermediateDataStruct(
67+
html=email_html,
68+
text=email_text,
69+
images=email_images,
70+
subject=email_subject,
71+
rsc_email_supress_report_attachment=supress_report_attachment,
72+
rsc_email_supress_scheduled=supress_scheduled,
73+
)
74+
75+
return iStruct
76+
77+
78+
# what to return?
79+
# consider malformed request?
80+
def send_quarto_email_with_gmail(
81+
username: str,
82+
password: str,
83+
json_path: str,
84+
recipients: list[str],
85+
):
86+
email_struct: IntermediateDataStruct = _read_quarto_email_json(json_path)
87+
email_struct.recipients = recipients
88+
send_struct_email_with_gmail(
89+
username=username, password=password, email_struct=email_struct
90+
)
91+
92+
93+
# Could also take creds object
94+
def send_struct_email_with_gmail(
95+
username: str, password: str, email_struct: IntermediateDataStruct
96+
):
97+
# Compose the email
98+
msg = MIMEMultipart("related")
99+
msg["Subject"] = email_struct.subject
100+
msg["From"] = username
101+
msg["To"] = ", ".join(email_struct.recipients) # Header must be a string
102+
103+
msg_alt = MIMEMultipart("alternative")
104+
msg.attach(msg_alt)
105+
msg_alt.attach(MIMEText(email_struct.html, "html"))
106+
107+
# Attach the plaintext
108+
if email_struct.text:
109+
msg_alt.attach(MIMEText(email_struct.text, "plain"))
110+
111+
# Attach images
112+
for image_name, image_base64 in email_struct.images.items():
113+
img_bytes = base64.b64decode(image_base64)
114+
img = MIMEImage(img_bytes, _subtype="png")
115+
img.add_header("Content-ID", f"<{image_name}>")
116+
img.add_header("Content-Disposition", "inline")
117+
118+
msg.attach(img)
119+
120+
121+
with smtplib.SMTP_SSL("smtp.gmail.com", 465) as server:
122+
server.login(username, password)
123+
server.sendmail(msg["From"], email_struct.recipients, msg.as_string())

0 commit comments

Comments
 (0)