[{"data":1,"prerenderedAt":3054},["ShallowReactive",2],{"doc:\u002Fautomating-reporting-workflows\u002Femailing-excel-reports-with-smtplib\u002Femail-an-excel-report-with-an-html-summary-body":3,"surround:\u002Fautomating-reporting-workflows\u002Femailing-excel-reports-with-smtplib\u002Femail-an-excel-report-with-an-html-summary-body":3046},{"id":4,"title":5,"body":6,"dateModified":3021,"datePublished":3021,"description":3022,"extension":3023,"faq":3024,"meta":3037,"navigation":278,"path":3038,"seo":3039,"slug":3042,"stem":3043,"type":3044,"__hash__":3045},"docs\u002Fautomating-reporting-workflows\u002Femailing-excel-reports-with-smtplib\u002Femail-an-excel-report-with-an-html-summary-body\u002Findex.md","Email an Excel Report with an HTML Summary Body",{"type":7,"value":8,"toc":3008},"minimark",[9,24,150,155,186,189,250,253,419,423,426,757,761,764,1478,1502,1506,1512,1969,1999,2014,2018,2086,2283,2286,2339,2342,2346,2349,2418,2603,2611,2615,2748,2752,2755,2761,2874,2880,2895,2903,2907,2910,2914,2923,2933,2946,2952,2964,2968,3005],[10,11,12,13,17,18,23],"p",{},"An emailed report that says \"please find attached\" makes every recipient open a spreadsheet to learn one number. Putting the headline figures in the body answers most people's question before they leave their inbox — and the ones who need detail still have the workbook. This guide builds that email: a multipart message with a plain-text part, an HTML body carrying a styled summary table, an embedded chart, and the ",[14,15,16],"code",{},".xlsx"," attached. It extends ",[19,20,22],"a",{"href":21},"\u002Fautomating-reporting-workflows\u002Femailing-excel-reports-with-smtplib\u002F","Emailing Excel Reports with smtplib",".",[25,26,35,36,35,40,35,44,35,51,35,60,35,67,35,76,35,82,35,89,35,94,35,99,35,103,35,108,35,112,35,119,35,124,35,129,35,133,35,138,35,143,35,146],"svg",{"viewBox":27,"role":28,"ariaLabel":29,"ariaLabelledBy":30,"xmlns":33,"style":34},"0 0 800 262","img","A multipart email structure: a mixed container holding an alternative part with plain text and HTML versions, plus the xlsx attachment and an embedded chart image.",[31,32],"mime-t","mime-d","http:\u002F\u002Fwww.w3.org\u002F2000\u002Fsvg","width:100%;max-width:800px;height:auto;display:block;margin:1.5rem auto;font-family:Inter,ui-sans-serif,system-ui,sans-serif","\n  ",[37,38,39],"title",{"id":31},"How the parts of a report email fit together",[41,42,43],"desc",{"id":32},"The outer message is multipart mixed. Inside it sits a multipart alternative containing two renderings of the same content: a plain-text version for clients and processors that cannot show HTML, and an HTML version with the styled summary table. Alongside the alternative sit the attachments — the xlsx workbook, and a chart image carrying a Content-ID so the HTML can reference it inline. Clients pick the richest alternative they support.",[45,46],"rect",{"x":47,"y":47,"width":48,"height":49,"fill":50},"0","800","262","#ffffff",[45,52],{"x":53,"y":54,"width":55,"height":56,"rx":53,"fill":57,"stroke":58,"style":59},"14","24","772","222","#f0f2f5","var(--line,#cdd5e6)","stroke-width:2px",[61,62,66],"text",{"x":63,"y":64,"style":65},"400","50","font-size:12.5px;font-weight:700;fill:var(--text,#172033);text-anchor:middle","multipart\u002Fmixed — the whole message",[45,68],{"x":69,"y":70,"width":71,"height":72,"rx":73,"fill":74,"stroke":75,"style":59},"38","64","418","160","12","#ebebfd","var(--brand,#5b5cf0)",[61,77,81],{"x":78,"y":79,"style":80},"247","90","font-size:12px;font-weight:700;fill:var(--brand-strong,#4338ca);text-anchor:middle","multipart\u002Falternative",[45,83],{"x":84,"y":85,"width":86,"height":87,"rx":88,"fill":50,"stroke":75},"62","104","370","48","9",[61,90,93],{"x":78,"y":91,"style":92},"124","font-size:11px;font-weight:700;fill:var(--brand-strong,#4338ca);text-anchor:middle","text\u002Fplain",[61,95,98],{"x":78,"y":96,"style":97},"142","font-size:10px;fill:var(--muted,#5b6780);text-anchor:middle","the same figures, no markup",[45,100],{"x":84,"y":72,"width":86,"height":87,"rx":88,"fill":101,"stroke":102,"style":59},"#d9f4f1","var(--teal,#0f9488)",[61,104,107],{"x":78,"y":105,"style":106},"180","font-size:11px;font-weight:700;fill:var(--teal-ink,#0b6157);text-anchor:middle","text\u002Fhtml",[61,109,111],{"x":78,"y":110,"style":97},"198","styled summary table, inline CSS",[45,113],{"x":114,"y":70,"width":115,"height":116,"rx":73,"fill":117,"stroke":118,"style":59},"476","286","74","#fdefd8","var(--gold,#b4740a)",[61,120,123],{"x":121,"y":79,"style":122},"619","font-size:12px;font-weight:700;fill:var(--gold-ink,#7a4e06);text-anchor:middle","the workbook",[61,125,128],{"x":121,"y":126,"style":127},"112","font-size:10px;fill:var(--text,#172033);text-anchor:middle","regional-2026-08.xlsx",[61,130,132],{"x":121,"y":131,"style":97},"129","spreadsheetml attachment",[45,134],{"x":114,"y":135,"width":115,"height":116,"rx":73,"fill":136,"stroke":137,"style":59},"150","#fee8f2","var(--accent,#f43f8f)",[61,139,142],{"x":121,"y":140,"style":141},"176","font-size:12px;font-weight:700;fill:var(--accent-ink,#be185d);text-anchor:middle","chart.png",[61,144,145],{"x":121,"y":110,"style":127},"Content-ID: \u003Ctrend>",[61,147,149],{"x":121,"y":148,"style":97},"215","referenced as cid:trend",[151,152,154],"h2",{"id":153},"prerequisites","Prerequisites",[156,157,162],"pre",{"className":158,"code":159,"language":160,"meta":161,"style":161},"language-bash shiki shiki-themes github-light github-dark-high-contrast","pip install pandas xlsxwriter matplotlib\n","bash","",[14,163,164],{"__ignoreMap":161},[165,166,169,173,177,180,183],"span",{"class":167,"line":168},"line",1,[165,170,172],{"class":171},"sMTad","pip",[165,174,176],{"class":175},"srMev"," install",[165,178,179],{"class":175}," pandas",[165,181,182],{"class":175}," xlsxwriter",[165,184,185],{"class":175}," matplotlib\n",[10,187,188],{},"Everything else is standard library. Credentials come from the environment:",[156,190,192],{"className":158,"code":191,"language":160,"meta":161,"style":161},"export SMTP_HOST=smtp.example.com\nexport SMTP_PORT=587\nexport SMTP_USER=reporting@example.com\nexport SMTP_PASSWORD='...'\n",[14,193,194,210,224,237],{"__ignoreMap":161},[165,195,196,200,204,207],{"class":167,"line":168},[165,197,199],{"class":198},"s-kum","export",[165,201,203],{"class":202},"skGVy"," SMTP_HOST",[165,205,206],{"class":198},"=",[165,208,209],{"class":202},"smtp.example.com\n",[165,211,213,215,218,220],{"class":167,"line":212},2,[165,214,199],{"class":198},[165,216,217],{"class":202}," SMTP_PORT",[165,219,206],{"class":198},[165,221,223],{"class":222},"sP0c6","587\n",[165,225,227,229,232,234],{"class":167,"line":226},3,[165,228,199],{"class":198},[165,230,231],{"class":202}," SMTP_USER",[165,233,206],{"class":198},[165,235,236],{"class":202},"reporting@example.com\n",[165,238,240,242,245,247],{"class":167,"line":239},4,[165,241,199],{"class":198},[165,243,244],{"class":202}," SMTP_PASSWORD",[165,246,206],{"class":198},[165,248,249],{"class":175},"'...'\n",[10,251,252],{},"Some figures to report:",[156,254,258],{"className":255,"code":256,"language":257,"meta":161,"style":161},"language-python shiki shiki-themes github-light github-dark-high-contrast","import pandas as pd\n\nsummary = pd.DataFrame({\n    \"region\": [\"North\", \"South\", \"West\", \"East\"],\n    \"revenue\": [5150.00, 4268.50, 3511.25, 2980.10],\n    \"target\": [4500.00, 4500.00, 3000.00, 3500.00],\n})\nsummary[\"variance\"] = summary[\"revenue\"] \u002F summary[\"target\"] - 1\n","python",[14,259,260,274,280,290,320,348,375,381],{"__ignoreMap":161},[165,261,262,265,268,271],{"class":167,"line":168},[165,263,264],{"class":198},"import",[165,266,267],{"class":202}," pandas ",[165,269,270],{"class":198},"as",[165,272,273],{"class":202}," pd\n",[165,275,276],{"class":167,"line":212},[165,277,279],{"emptyLinePlaceholder":278},true,"\n",[165,281,282,285,287],{"class":167,"line":226},[165,283,284],{"class":202},"summary ",[165,286,206],{"class":198},[165,288,289],{"class":202}," pd.DataFrame({\n",[165,291,292,295,298,301,304,307,309,312,314,317],{"class":167,"line":239},[165,293,294],{"class":175},"    \"region\"",[165,296,297],{"class":202},": [",[165,299,300],{"class":175},"\"North\"",[165,302,303],{"class":202},", ",[165,305,306],{"class":175},"\"South\"",[165,308,303],{"class":202},[165,310,311],{"class":175},"\"West\"",[165,313,303],{"class":202},[165,315,316],{"class":175},"\"East\"",[165,318,319],{"class":202},"],\n",[165,321,323,326,328,331,333,336,338,341,343,346],{"class":167,"line":322},5,[165,324,325],{"class":175},"    \"revenue\"",[165,327,297],{"class":202},[165,329,330],{"class":222},"5150.00",[165,332,303],{"class":202},[165,334,335],{"class":222},"4268.50",[165,337,303],{"class":202},[165,339,340],{"class":222},"3511.25",[165,342,303],{"class":202},[165,344,345],{"class":222},"2980.10",[165,347,319],{"class":202},[165,349,351,354,356,359,361,363,365,368,370,373],{"class":167,"line":350},6,[165,352,353],{"class":175},"    \"target\"",[165,355,297],{"class":202},[165,357,358],{"class":222},"4500.00",[165,360,303],{"class":202},[165,362,358],{"class":222},[165,364,303],{"class":202},[165,366,367],{"class":222},"3000.00",[165,369,303],{"class":202},[165,371,372],{"class":222},"3500.00",[165,374,319],{"class":202},[165,376,378],{"class":167,"line":377},7,[165,379,380],{"class":202},"})\n",[165,382,384,387,390,393,395,398,401,403,406,408,411,413,416],{"class":167,"line":383},8,[165,385,386],{"class":202},"summary[",[165,388,389],{"class":175},"\"variance\"",[165,391,392],{"class":202},"] ",[165,394,206],{"class":198},[165,396,397],{"class":202}," summary[",[165,399,400],{"class":175},"\"revenue\"",[165,402,392],{"class":202},[165,404,405],{"class":198},"\u002F",[165,407,397],{"class":202},[165,409,410],{"class":175},"\"target\"",[165,412,392],{"class":202},[165,414,415],{"class":198},"-",[165,417,418],{"class":222}," 1\n",[151,420,422],{"id":421},"step-1-write-the-plain-text-version-first","Step 1 — Write the plain-text version first",[10,424,425],{},"Starting with text keeps you honest about what actually matters — if a figure does not earn a line here, it does not belong in the HTML either:",[156,427,429],{"className":255,"code":428,"language":257,"meta":161,"style":161},"def text_body(summary, period=\"August 2026\"):\n    \"\"\"The message as plain text. Written first, deliberately.\"\"\"\n    total = summary[\"revenue\"].sum()\n    target = summary[\"target\"].sum()\n    variance = total \u002F target - 1\n\n    lines = [\n        f\"Regional revenue — {period}\",\n        \"\",\n        f\"Total revenue   {total:>12,.2f}\",\n        f\"Target          {target:>12,.2f}\",\n        f\"Variance        {variance:>11.1%}\",\n        \"\",\n        \"By region:\",\n    ]\n    for row in summary.itertuples(index=False):\n        lines.append(\n            f\"  {row.region:\u003C8} {row.revenue:>10,.2f}  ({row.variance:+.1%})\"\n        )\n    lines += [\"\", \"The full detail is attached.\"]\n    return \"\\n\".join(lines)\n",[14,430,431,451,456,470,483,502,506,516,540,548,570,591,613,620,628,634,660,666,712,718,740],{"__ignoreMap":161},[165,432,433,436,440,443,445,448],{"class":167,"line":168},[165,434,435],{"class":198},"def",[165,437,439],{"class":438},"s_Opv"," text_body",[165,441,442],{"class":202},"(summary, period",[165,444,206],{"class":198},[165,446,447],{"class":175},"\"August 2026\"",[165,449,450],{"class":202},"):\n",[165,452,453],{"class":167,"line":212},[165,454,455],{"class":175},"    \"\"\"The message as plain text. Written first, deliberately.\"\"\"\n",[165,457,458,461,463,465,467],{"class":167,"line":226},[165,459,460],{"class":202},"    total ",[165,462,206],{"class":198},[165,464,397],{"class":202},[165,466,400],{"class":175},[165,468,469],{"class":202},"].sum()\n",[165,471,472,475,477,479,481],{"class":167,"line":239},[165,473,474],{"class":202},"    target ",[165,476,206],{"class":198},[165,478,397],{"class":202},[165,480,410],{"class":175},[165,482,469],{"class":202},[165,484,485,488,490,493,495,498,500],{"class":167,"line":322},[165,486,487],{"class":202},"    variance ",[165,489,206],{"class":198},[165,491,492],{"class":202}," total ",[165,494,405],{"class":198},[165,496,497],{"class":202}," target ",[165,499,415],{"class":198},[165,501,418],{"class":222},[165,503,504],{"class":167,"line":350},[165,505,279],{"emptyLinePlaceholder":278},[165,507,508,511,513],{"class":167,"line":377},[165,509,510],{"class":202},"    lines ",[165,512,206],{"class":198},[165,514,515],{"class":202}," [\n",[165,517,518,521,524,528,531,534,537],{"class":167,"line":383},[165,519,520],{"class":198},"        f",[165,522,523],{"class":175},"\"Regional revenue — ",[165,525,527],{"class":526},"sSjpA","{",[165,529,530],{"class":202},"period",[165,532,533],{"class":526},"}",[165,535,536],{"class":175},"\"",[165,538,539],{"class":202},",\n",[165,541,543,546],{"class":167,"line":542},9,[165,544,545],{"class":175},"        \"\"",[165,547,539],{"class":202},[165,549,551,553,556,558,561,564,566,568],{"class":167,"line":550},10,[165,552,520],{"class":198},[165,554,555],{"class":175},"\"Total revenue   ",[165,557,527],{"class":526},[165,559,560],{"class":202},"total",[165,562,563],{"class":198},":>12,.2f",[165,565,533],{"class":526},[165,567,536],{"class":175},[165,569,539],{"class":202},[165,571,573,575,578,580,583,585,587,589],{"class":167,"line":572},11,[165,574,520],{"class":198},[165,576,577],{"class":175},"\"Target          ",[165,579,527],{"class":526},[165,581,582],{"class":202},"target",[165,584,563],{"class":198},[165,586,533],{"class":526},[165,588,536],{"class":175},[165,590,539],{"class":202},[165,592,594,596,599,601,604,607,609,611],{"class":167,"line":593},12,[165,595,520],{"class":198},[165,597,598],{"class":175},"\"Variance        ",[165,600,527],{"class":526},[165,602,603],{"class":202},"variance",[165,605,606],{"class":198},":>11.1%",[165,608,533],{"class":526},[165,610,536],{"class":175},[165,612,539],{"class":202},[165,614,616,618],{"class":167,"line":615},13,[165,617,545],{"class":175},[165,619,539],{"class":202},[165,621,623,626],{"class":167,"line":622},14,[165,624,625],{"class":175},"        \"By region:\"",[165,627,539],{"class":202},[165,629,631],{"class":167,"line":630},15,[165,632,633],{"class":202},"    ]\n",[165,635,637,640,643,646,649,653,655,658],{"class":167,"line":636},16,[165,638,639],{"class":198},"    for",[165,641,642],{"class":202}," row ",[165,644,645],{"class":198},"in",[165,647,648],{"class":202}," summary.itertuples(",[165,650,652],{"class":651},"sa561","index",[165,654,206],{"class":198},[165,656,657],{"class":222},"False",[165,659,450],{"class":202},[165,661,663],{"class":167,"line":662},17,[165,664,665],{"class":202},"        lines.append(\n",[165,667,669,672,675,677,680,683,685,688,691,694,696,699,701,704,707,709],{"class":167,"line":668},18,[165,670,671],{"class":198},"            f",[165,673,674],{"class":175},"\"  ",[165,676,527],{"class":526},[165,678,679],{"class":202},"row.region",[165,681,682],{"class":198},":\u003C8",[165,684,533],{"class":526},[165,686,687],{"class":526}," {",[165,689,690],{"class":202},"row.revenue",[165,692,693],{"class":198},":>10,.2f",[165,695,533],{"class":526},[165,697,698],{"class":175},"  (",[165,700,527],{"class":526},[165,702,703],{"class":202},"row.variance",[165,705,706],{"class":198},":+.1%",[165,708,533],{"class":526},[165,710,711],{"class":175},")\"\n",[165,713,715],{"class":167,"line":714},19,[165,716,717],{"class":202},"        )\n",[165,719,721,723,726,729,732,734,737],{"class":167,"line":720},20,[165,722,510],{"class":202},[165,724,725],{"class":198},"+=",[165,727,728],{"class":202}," [",[165,730,731],{"class":175},"\"\"",[165,733,303],{"class":202},[165,735,736],{"class":175},"\"The full detail is attached.\"",[165,738,739],{"class":202},"]\n",[165,741,743,746,749,752,754],{"class":167,"line":742},21,[165,744,745],{"class":198},"    return",[165,747,748],{"class":175}," \"",[165,750,751],{"class":526},"\\n",[165,753,536],{"class":175},[165,755,756],{"class":202},".join(lines)\n",[151,758,760],{"id":759},"step-2-build-the-html-with-inline-styles","Step 2 — Build the HTML with inline styles",[10,762,763],{},"Mail clients strip stylesheets, ignore most modern CSS, and in several cases still lay out with tables. Every style has to be inline, and the layout has to be a table:",[156,765,767],{"className":255,"code":766,"language":257,"meta":161,"style":161},"import html\n\nCELL = \"padding:8px 12px;border-bottom:1px solid #cdd5e6;font-size:14px;\"\nHEAD = (\"padding:8px 12px;background-color:#4338ca;color:#ffffff;\"\n        \"font-size:13px;font-weight:bold;text-align:left;\")\n\ndef html_body(summary, period=\"August 2026\", chart_cid=None):\n    \"\"\"The message as HTML. Every style inline; tables for layout.\"\"\"\n    total = summary[\"revenue\"].sum()\n    target = summary[\"target\"].sum()\n    variance = total \u002F target - 1\n    tone = \"#0b6157\" if variance >= 0 else \"#be185d\"\n\n    rows = []\n    for row in summary.itertuples(index=False):\n        colour = \"#0b6157\" if row.variance >= 0 else \"#be185d\"\n        rows.append(\n            f'\u003Ctr>'\n            f'\u003Ctd style=\"{CELL}\">{html.escape(str(row.region))}\u003C\u002Ftd>'\n            f'\u003Ctd style=\"{CELL}text-align:right;\">{row.revenue:,.2f}\u003C\u002Ftd>'\n            f'\u003Ctd style=\"{CELL}text-align:right;color:{colour};'\n            f'font-weight:bold;\">{row.variance:+.1%}\u003C\u002Ftd>'\n            f'\u003C\u002Ftr>'\n        )\n\n    chart = (\n        f'\u003Cimg src=\"cid:{chart_cid}\" alt=\"Revenue by region\" '\n        f'style=\"display:block;width:100%;max-width:520px;height:auto;'\n        f'margin:20px 0;border:1px solid #cdd5e6;border-radius:6px;\">'\n        if chart_cid else \"\"\n    )\n\n    return f\"\"\"\\\n\u003C!doctype html>\n\u003Chtml>\u003Cbody style=\"margin:0;padding:0;background-color:#f4f7ff;\">\n\u003Ctable role=\"presentation\" width=\"100%\" cellpadding=\"0\" cellspacing=\"0\"\n       style=\"background-color:#f4f7ff;padding:24px 0;\">\n\u003Ctr>\u003Ctd align=\"center\">\n\u003Ctable role=\"presentation\" width=\"560\" cellpadding=\"0\" cellspacing=\"0\"\n       style=\"width:560px;max-width:100%;background-color:#ffffff;\n              border-radius:10px;padding:24px;\n              font-family:Arial,Helvetica,sans-serif;color:#172033;\">\n  \u003Ctr>\u003Ctd>\n    \u003Cp style=\"margin:0 0 4px;font-size:19px;font-weight:bold;color:#4338ca;\">\n      Regional revenue\u003C\u002Fp>\n    \u003Cp style=\"margin:0 0 20px;font-size:13px;color:#5b6780;\">\n      {html.escape(period)}\u003C\u002Fp>\n    \u003Cp style=\"margin:0 0 20px;font-size:15px;\">\n      Total \u003Cstrong>{total:,.2f}\u003C\u002Fstrong> against a target of\n      {target:,.2f} &mdash;\n      \u003Cstrong style=\"color:{tone};\">{variance:+.1%}\u003C\u002Fstrong>.\n    \u003C\u002Fp>\n    {chart}\n    \u003Ctable role=\"presentation\" width=\"100%\" cellpadding=\"0\" cellspacing=\"0\"\n           style=\"border-collapse:collapse;\">\n      \u003Ctr>\n        \u003Cth style=\"{HEAD}\">Region\u003C\u002Fth>\n        \u003Cth style=\"{HEAD}text-align:right;\">Revenue\u003C\u002Fth>\n        \u003Cth style=\"{HEAD}text-align:right;\">vs target\u003C\u002Fth>\n      \u003C\u002Ftr>\n      {''.join(rows)}\n    \u003C\u002Ftable>\n    \u003Cp style=\"margin:20px 0 0;font-size:13px;color:#5b6780;\">\n      The full detail is attached.\u003C\u002Fp>\n  \u003C\u002Ftd>\u003C\u002Ftr>\n\u003C\u002Ftable>\n\u003C\u002Ftd>\u003C\u002Ftr>\u003C\u002Ftable>\n\u003C\u002Fbody>\u003C\u002Fhtml>\"\"\"\n",[14,768,769,776,780,791,804,812,816,839,844,856,868,884,912,916,926,944,966,971,978,1010,1036,1061,1079,1087,1092,1097,1108,1126,1134,1142,1157,1163,1168,1182,1188,1194,1200,1206,1212,1218,1224,1230,1236,1242,1248,1254,1260,1274,1280,1297,1311,1338,1344,1356,1362,1368,1374,1389,1403,1417,1423,1436,1442,1448,1454,1460,1466,1472],{"__ignoreMap":161},[165,770,771,773],{"class":167,"line":168},[165,772,264],{"class":198},[165,774,775],{"class":202}," html\n",[165,777,778],{"class":167,"line":212},[165,779,279],{"emptyLinePlaceholder":278},[165,781,782,785,788],{"class":167,"line":226},[165,783,784],{"class":222},"CELL",[165,786,787],{"class":198}," =",[165,789,790],{"class":175}," \"padding:8px 12px;border-bottom:1px solid #cdd5e6;font-size:14px;\"\n",[165,792,793,796,798,801],{"class":167,"line":239},[165,794,795],{"class":222},"HEAD",[165,797,787],{"class":198},[165,799,800],{"class":202}," (",[165,802,803],{"class":175},"\"padding:8px 12px;background-color:#4338ca;color:#ffffff;\"\n",[165,805,806,809],{"class":167,"line":322},[165,807,808],{"class":175},"        \"font-size:13px;font-weight:bold;text-align:left;\"",[165,810,811],{"class":202},")\n",[165,813,814],{"class":167,"line":350},[165,815,279],{"emptyLinePlaceholder":278},[165,817,818,820,823,825,827,829,832,834,837],{"class":167,"line":377},[165,819,435],{"class":198},[165,821,822],{"class":438}," html_body",[165,824,442],{"class":202},[165,826,206],{"class":198},[165,828,447],{"class":175},[165,830,831],{"class":202},", chart_cid",[165,833,206],{"class":198},[165,835,836],{"class":222},"None",[165,838,450],{"class":202},[165,840,841],{"class":167,"line":383},[165,842,843],{"class":175},"    \"\"\"The message as HTML. Every style inline; tables for layout.\"\"\"\n",[165,845,846,848,850,852,854],{"class":167,"line":542},[165,847,460],{"class":202},[165,849,206],{"class":198},[165,851,397],{"class":202},[165,853,400],{"class":175},[165,855,469],{"class":202},[165,857,858,860,862,864,866],{"class":167,"line":550},[165,859,474],{"class":202},[165,861,206],{"class":198},[165,863,397],{"class":202},[165,865,410],{"class":175},[165,867,469],{"class":202},[165,869,870,872,874,876,878,880,882],{"class":167,"line":572},[165,871,487],{"class":202},[165,873,206],{"class":198},[165,875,492],{"class":202},[165,877,405],{"class":198},[165,879,497],{"class":202},[165,881,415],{"class":198},[165,883,418],{"class":222},[165,885,886,889,891,894,897,900,903,906,909],{"class":167,"line":593},[165,887,888],{"class":202},"    tone ",[165,890,206],{"class":198},[165,892,893],{"class":175}," \"#0b6157\"",[165,895,896],{"class":198}," if",[165,898,899],{"class":202}," variance ",[165,901,902],{"class":198},">=",[165,904,905],{"class":222}," 0",[165,907,908],{"class":198}," else",[165,910,911],{"class":175}," \"#be185d\"\n",[165,913,914],{"class":167,"line":615},[165,915,279],{"emptyLinePlaceholder":278},[165,917,918,921,923],{"class":167,"line":622},[165,919,920],{"class":202},"    rows ",[165,922,206],{"class":198},[165,924,925],{"class":202}," []\n",[165,927,928,930,932,934,936,938,940,942],{"class":167,"line":630},[165,929,639],{"class":198},[165,931,642],{"class":202},[165,933,645],{"class":198},[165,935,648],{"class":202},[165,937,652],{"class":651},[165,939,206],{"class":198},[165,941,657],{"class":222},[165,943,450],{"class":202},[165,945,946,949,951,953,955,958,960,962,964],{"class":167,"line":636},[165,947,948],{"class":202},"        colour ",[165,950,206],{"class":198},[165,952,893],{"class":175},[165,954,896],{"class":198},[165,956,957],{"class":202}," row.variance ",[165,959,902],{"class":198},[165,961,905],{"class":222},[165,963,908],{"class":198},[165,965,911],{"class":175},[165,967,968],{"class":167,"line":662},[165,969,970],{"class":202},"        rows.append(\n",[165,972,973,975],{"class":167,"line":668},[165,974,671],{"class":198},[165,976,977],{"class":175},"'\u003Ctr>'\n",[165,979,980,982,985,987,989,991,994,996,999,1002,1005,1007],{"class":167,"line":714},[165,981,671],{"class":198},[165,983,984],{"class":175},"'\u003Ctd style=\"",[165,986,527],{"class":526},[165,988,784],{"class":222},[165,990,533],{"class":526},[165,992,993],{"class":175},"\">",[165,995,527],{"class":526},[165,997,998],{"class":202},"html.escape(",[165,1000,1001],{"class":222},"str",[165,1003,1004],{"class":202},"(row.region))",[165,1006,533],{"class":526},[165,1008,1009],{"class":175},"\u003C\u002Ftd>'\n",[165,1011,1012,1014,1016,1018,1020,1022,1025,1027,1029,1032,1034],{"class":167,"line":720},[165,1013,671],{"class":198},[165,1015,984],{"class":175},[165,1017,527],{"class":526},[165,1019,784],{"class":222},[165,1021,533],{"class":526},[165,1023,1024],{"class":175},"text-align:right;\">",[165,1026,527],{"class":526},[165,1028,690],{"class":202},[165,1030,1031],{"class":198},":,.2f",[165,1033,533],{"class":526},[165,1035,1009],{"class":175},[165,1037,1038,1040,1042,1044,1046,1048,1051,1053,1056,1058],{"class":167,"line":742},[165,1039,671],{"class":198},[165,1041,984],{"class":175},[165,1043,527],{"class":526},[165,1045,784],{"class":222},[165,1047,533],{"class":526},[165,1049,1050],{"class":175},"text-align:right;color:",[165,1052,527],{"class":526},[165,1054,1055],{"class":202},"colour",[165,1057,533],{"class":526},[165,1059,1060],{"class":175},";'\n",[165,1062,1064,1066,1069,1071,1073,1075,1077],{"class":167,"line":1063},22,[165,1065,671],{"class":198},[165,1067,1068],{"class":175},"'font-weight:bold;\">",[165,1070,527],{"class":526},[165,1072,703],{"class":202},[165,1074,706],{"class":198},[165,1076,533],{"class":526},[165,1078,1009],{"class":175},[165,1080,1082,1084],{"class":167,"line":1081},23,[165,1083,671],{"class":198},[165,1085,1086],{"class":175},"'\u003C\u002Ftr>'\n",[165,1088,1090],{"class":167,"line":1089},24,[165,1091,717],{"class":202},[165,1093,1095],{"class":167,"line":1094},25,[165,1096,279],{"emptyLinePlaceholder":278},[165,1098,1100,1103,1105],{"class":167,"line":1099},26,[165,1101,1102],{"class":202},"    chart ",[165,1104,206],{"class":198},[165,1106,1107],{"class":202}," (\n",[165,1109,1111,1113,1116,1118,1121,1123],{"class":167,"line":1110},27,[165,1112,520],{"class":198},[165,1114,1115],{"class":175},"'\u003Cimg src=\"cid:",[165,1117,527],{"class":526},[165,1119,1120],{"class":202},"chart_cid",[165,1122,533],{"class":526},[165,1124,1125],{"class":175},"\" alt=\"Revenue by region\" '\n",[165,1127,1129,1131],{"class":167,"line":1128},28,[165,1130,520],{"class":198},[165,1132,1133],{"class":175},"'style=\"display:block;width:100%;max-width:520px;height:auto;'\n",[165,1135,1137,1139],{"class":167,"line":1136},29,[165,1138,520],{"class":198},[165,1140,1141],{"class":175},"'margin:20px 0;border:1px solid #cdd5e6;border-radius:6px;\">'\n",[165,1143,1145,1148,1151,1154],{"class":167,"line":1144},30,[165,1146,1147],{"class":198},"        if",[165,1149,1150],{"class":202}," chart_cid ",[165,1152,1153],{"class":198},"else",[165,1155,1156],{"class":175}," \"\"\n",[165,1158,1160],{"class":167,"line":1159},31,[165,1161,1162],{"class":202},"    )\n",[165,1164,1166],{"class":167,"line":1165},32,[165,1167,279],{"emptyLinePlaceholder":278},[165,1169,1171,1173,1176,1179],{"class":167,"line":1170},33,[165,1172,745],{"class":198},[165,1174,1175],{"class":198}," f",[165,1177,1178],{"class":175},"\"\"\"",[165,1180,1181],{"class":222},"\\\n",[165,1183,1185],{"class":167,"line":1184},34,[165,1186,1187],{"class":175},"\u003C!doctype html>\n",[165,1189,1191],{"class":167,"line":1190},35,[165,1192,1193],{"class":175},"\u003Chtml>\u003Cbody style=\"margin:0;padding:0;background-color:#f4f7ff;\">\n",[165,1195,1197],{"class":167,"line":1196},36,[165,1198,1199],{"class":175},"\u003Ctable role=\"presentation\" width=\"100%\" cellpadding=\"0\" cellspacing=\"0\"\n",[165,1201,1203],{"class":167,"line":1202},37,[165,1204,1205],{"class":175},"       style=\"background-color:#f4f7ff;padding:24px 0;\">\n",[165,1207,1209],{"class":167,"line":1208},38,[165,1210,1211],{"class":175},"\u003Ctr>\u003Ctd align=\"center\">\n",[165,1213,1215],{"class":167,"line":1214},39,[165,1216,1217],{"class":175},"\u003Ctable role=\"presentation\" width=\"560\" cellpadding=\"0\" cellspacing=\"0\"\n",[165,1219,1221],{"class":167,"line":1220},40,[165,1222,1223],{"class":175},"       style=\"width:560px;max-width:100%;background-color:#ffffff;\n",[165,1225,1227],{"class":167,"line":1226},41,[165,1228,1229],{"class":175},"              border-radius:10px;padding:24px;\n",[165,1231,1233],{"class":167,"line":1232},42,[165,1234,1235],{"class":175},"              font-family:Arial,Helvetica,sans-serif;color:#172033;\">\n",[165,1237,1239],{"class":167,"line":1238},43,[165,1240,1241],{"class":175},"  \u003Ctr>\u003Ctd>\n",[165,1243,1245],{"class":167,"line":1244},44,[165,1246,1247],{"class":175},"    \u003Cp style=\"margin:0 0 4px;font-size:19px;font-weight:bold;color:#4338ca;\">\n",[165,1249,1251],{"class":167,"line":1250},45,[165,1252,1253],{"class":175},"      Regional revenue\u003C\u002Fp>\n",[165,1255,1257],{"class":167,"line":1256},46,[165,1258,1259],{"class":175},"    \u003Cp style=\"margin:0 0 20px;font-size:13px;color:#5b6780;\">\n",[165,1261,1263,1266,1269,1271],{"class":167,"line":1262},47,[165,1264,1265],{"class":526},"      {",[165,1267,1268],{"class":202},"html.escape(period)",[165,1270,533],{"class":526},[165,1272,1273],{"class":175},"\u003C\u002Fp>\n",[165,1275,1277],{"class":167,"line":1276},48,[165,1278,1279],{"class":175},"    \u003Cp style=\"margin:0 0 20px;font-size:15px;\">\n",[165,1281,1283,1286,1288,1290,1292,1294],{"class":167,"line":1282},49,[165,1284,1285],{"class":175},"      Total \u003Cstrong>",[165,1287,527],{"class":526},[165,1289,560],{"class":202},[165,1291,1031],{"class":198},[165,1293,533],{"class":526},[165,1295,1296],{"class":175},"\u003C\u002Fstrong> against a target of\n",[165,1298,1300,1302,1304,1306,1308],{"class":167,"line":1299},50,[165,1301,1265],{"class":526},[165,1303,582],{"class":202},[165,1305,1031],{"class":198},[165,1307,533],{"class":526},[165,1309,1310],{"class":175}," &mdash;\n",[165,1312,1314,1317,1319,1322,1324,1327,1329,1331,1333,1335],{"class":167,"line":1313},51,[165,1315,1316],{"class":175},"      \u003Cstrong style=\"color:",[165,1318,527],{"class":526},[165,1320,1321],{"class":202},"tone",[165,1323,533],{"class":526},[165,1325,1326],{"class":175},";\">",[165,1328,527],{"class":526},[165,1330,603],{"class":202},[165,1332,706],{"class":198},[165,1334,533],{"class":526},[165,1336,1337],{"class":175},"\u003C\u002Fstrong>.\n",[165,1339,1341],{"class":167,"line":1340},52,[165,1342,1343],{"class":175},"    \u003C\u002Fp>\n",[165,1345,1347,1350,1353],{"class":167,"line":1346},53,[165,1348,1349],{"class":526},"    {",[165,1351,1352],{"class":202},"chart",[165,1354,1355],{"class":526},"}\n",[165,1357,1359],{"class":167,"line":1358},54,[165,1360,1361],{"class":175},"    \u003Ctable role=\"presentation\" width=\"100%\" cellpadding=\"0\" cellspacing=\"0\"\n",[165,1363,1365],{"class":167,"line":1364},55,[165,1366,1367],{"class":175},"           style=\"border-collapse:collapse;\">\n",[165,1369,1371],{"class":167,"line":1370},56,[165,1372,1373],{"class":175},"      \u003Ctr>\n",[165,1375,1377,1380,1382,1384,1386],{"class":167,"line":1376},57,[165,1378,1379],{"class":175},"        \u003Cth style=\"",[165,1381,527],{"class":526},[165,1383,795],{"class":222},[165,1385,533],{"class":526},[165,1387,1388],{"class":175},"\">Region\u003C\u002Fth>\n",[165,1390,1392,1394,1396,1398,1400],{"class":167,"line":1391},58,[165,1393,1379],{"class":175},[165,1395,527],{"class":526},[165,1397,795],{"class":222},[165,1399,533],{"class":526},[165,1401,1402],{"class":175},"text-align:right;\">Revenue\u003C\u002Fth>\n",[165,1404,1406,1408,1410,1412,1414],{"class":167,"line":1405},59,[165,1407,1379],{"class":175},[165,1409,527],{"class":526},[165,1411,795],{"class":222},[165,1413,533],{"class":526},[165,1415,1416],{"class":175},"text-align:right;\">vs target\u003C\u002Fth>\n",[165,1418,1420],{"class":167,"line":1419},60,[165,1421,1422],{"class":175},"      \u003C\u002Ftr>\n",[165,1424,1426,1428,1431,1434],{"class":167,"line":1425},61,[165,1427,1265],{"class":526},[165,1429,1430],{"class":175},"''",[165,1432,1433],{"class":202},".join(rows)",[165,1435,1355],{"class":526},[165,1437,1439],{"class":167,"line":1438},62,[165,1440,1441],{"class":175},"    \u003C\u002Ftable>\n",[165,1443,1445],{"class":167,"line":1444},63,[165,1446,1447],{"class":175},"    \u003Cp style=\"margin:20px 0 0;font-size:13px;color:#5b6780;\">\n",[165,1449,1451],{"class":167,"line":1450},64,[165,1452,1453],{"class":175},"      The full detail is attached.\u003C\u002Fp>\n",[165,1455,1457],{"class":167,"line":1456},65,[165,1458,1459],{"class":175},"  \u003C\u002Ftd>\u003C\u002Ftr>\n",[165,1461,1463],{"class":167,"line":1462},66,[165,1464,1465],{"class":175},"\u003C\u002Ftable>\n",[165,1467,1469],{"class":167,"line":1468},67,[165,1470,1471],{"class":175},"\u003C\u002Ftd>\u003C\u002Ftr>\u003C\u002Ftable>\n",[165,1473,1475],{"class":167,"line":1474},68,[165,1476,1477],{"class":175},"\u003C\u002Fbody>\u003C\u002Fhtml>\"\"\"\n",[10,1479,1480,1481,1485,1486,1489,1490,1493,1494,1497,1498,1501],{},"Three constraints worth internalising. ",[1482,1483,1484],"strong",{},"Escape anything from the data"," — a region called ",[14,1487,1488],{},"Smith & Co"," breaks the markup otherwise, and ",[14,1491,1492],{},"html.escape"," costs nothing. ",[1482,1495,1496],{},"Fix the outer table's width in pixels",", because percentage widths render unpredictably across clients. And ",[1482,1499,1500],{},"avoid flexbox, grid and background images",": several widely used clients support none of them, and the fallback is usually a broken layout rather than a plain one.",[151,1503,1505],{"id":1504},"step-3-assemble-the-message","Step 3 — Assemble the message",[10,1507,1508,1511],{},[14,1509,1510],{},"EmailMessage"," builds the multipart structure for you, provided the calls happen in the right order:",[156,1513,1515],{"className":255,"code":1514,"language":257,"meta":161,"style":161},"import mimetypes\nfrom email.message import EmailMessage\nfrom email.utils import formataddr, make_msgid\nfrom pathlib import Path\n\nXLSX = \"application\u002Fvnd.openxmlformats-officedocument.spreadsheetml.sheet\"\n\ndef build_message(summary, attachment, sender, recipients,\n                  period=\"August 2026\", chart_path=None):\n    \"\"\"A multipart email with a text part, an HTML part and attachments.\"\"\"\n    msg = EmailMessage()\n    msg[\"From\"] = formataddr((\"Reporting\", sender))\n    msg[\"To\"] = \", \".join(recipients)\n    msg[\"Subject\"] = f\"Regional revenue — {period}\"\n\n    # 1. Plain text becomes the message body.\n    msg.set_content(text_body(summary, period))\n\n    # 2. A Content-ID for the chart, if there is one.\n    cid = None\n    if chart_path:\n        cid = make_msgid(domain=\"reports.example.com\")\n\n    # 3. HTML is added as an alternative to the text.\n    msg.add_alternative(\n        html_body(summary, period, chart_cid=cid.strip(\"\u003C>\") if cid else None),\n        subtype=\"html\",\n    )\n\n    # 4. The chart is related to the HTML part, not to the message.\n    if chart_path:\n        html_part = msg.get_payload()[1]\n        html_part.add_related(\n            Path(chart_path).read_bytes(),\n            maintype=\"image\", subtype=\"png\", cid=cid,\n            filename=Path(chart_path).name,\n        )\n\n    # 5. The workbook attaches to the message itself.\n    data = Path(attachment).read_bytes()\n    msg.add_attachment(data, maintype=\"application\",\n                       subtype=XLSX.split(\"\u002F\", 1)[1],\n                       filename=Path(attachment).name)\n    return msg\n",[14,1516,1517,1524,1537,1549,1561,1565,1575,1579,1589,1607,1612,1622,1643,1660,1684,1688,1694,1699,1703,1708,1718,1726,1746,1750,1755,1760,1792,1804,1808,1812,1817,1823,1838,1843,1848,1878,1888,1892,1896,1901,1911,1926,1952,1962],{"__ignoreMap":161},[165,1518,1519,1521],{"class":167,"line":168},[165,1520,264],{"class":198},[165,1522,1523],{"class":202}," mimetypes\n",[165,1525,1526,1529,1532,1534],{"class":167,"line":212},[165,1527,1528],{"class":198},"from",[165,1530,1531],{"class":202}," email.message ",[165,1533,264],{"class":198},[165,1535,1536],{"class":202}," EmailMessage\n",[165,1538,1539,1541,1544,1546],{"class":167,"line":226},[165,1540,1528],{"class":198},[165,1542,1543],{"class":202}," email.utils ",[165,1545,264],{"class":198},[165,1547,1548],{"class":202}," formataddr, make_msgid\n",[165,1550,1551,1553,1556,1558],{"class":167,"line":239},[165,1552,1528],{"class":198},[165,1554,1555],{"class":202}," pathlib ",[165,1557,264],{"class":198},[165,1559,1560],{"class":202}," Path\n",[165,1562,1563],{"class":167,"line":322},[165,1564,279],{"emptyLinePlaceholder":278},[165,1566,1567,1570,1572],{"class":167,"line":350},[165,1568,1569],{"class":222},"XLSX",[165,1571,787],{"class":198},[165,1573,1574],{"class":175}," \"application\u002Fvnd.openxmlformats-officedocument.spreadsheetml.sheet\"\n",[165,1576,1577],{"class":167,"line":377},[165,1578,279],{"emptyLinePlaceholder":278},[165,1580,1581,1583,1586],{"class":167,"line":383},[165,1582,435],{"class":198},[165,1584,1585],{"class":438}," build_message",[165,1587,1588],{"class":202},"(summary, attachment, sender, recipients,\n",[165,1590,1591,1594,1596,1598,1601,1603,1605],{"class":167,"line":542},[165,1592,1593],{"class":202},"                  period",[165,1595,206],{"class":198},[165,1597,447],{"class":175},[165,1599,1600],{"class":202},", chart_path",[165,1602,206],{"class":198},[165,1604,836],{"class":222},[165,1606,450],{"class":202},[165,1608,1609],{"class":167,"line":550},[165,1610,1611],{"class":175},"    \"\"\"A multipart email with a text part, an HTML part and attachments.\"\"\"\n",[165,1613,1614,1617,1619],{"class":167,"line":572},[165,1615,1616],{"class":202},"    msg ",[165,1618,206],{"class":198},[165,1620,1621],{"class":202}," EmailMessage()\n",[165,1623,1624,1627,1630,1632,1634,1637,1640],{"class":167,"line":593},[165,1625,1626],{"class":202},"    msg[",[165,1628,1629],{"class":175},"\"From\"",[165,1631,392],{"class":202},[165,1633,206],{"class":198},[165,1635,1636],{"class":202}," formataddr((",[165,1638,1639],{"class":175},"\"Reporting\"",[165,1641,1642],{"class":202},", sender))\n",[165,1644,1645,1647,1650,1652,1654,1657],{"class":167,"line":615},[165,1646,1626],{"class":202},[165,1648,1649],{"class":175},"\"To\"",[165,1651,392],{"class":202},[165,1653,206],{"class":198},[165,1655,1656],{"class":175}," \", \"",[165,1658,1659],{"class":202},".join(recipients)\n",[165,1661,1662,1664,1667,1669,1671,1673,1675,1677,1679,1681],{"class":167,"line":622},[165,1663,1626],{"class":202},[165,1665,1666],{"class":175},"\"Subject\"",[165,1668,392],{"class":202},[165,1670,206],{"class":198},[165,1672,1175],{"class":198},[165,1674,523],{"class":175},[165,1676,527],{"class":526},[165,1678,530],{"class":202},[165,1680,533],{"class":526},[165,1682,1683],{"class":175},"\"\n",[165,1685,1686],{"class":167,"line":630},[165,1687,279],{"emptyLinePlaceholder":278},[165,1689,1690],{"class":167,"line":636},[165,1691,1693],{"class":1692},"s-wDw","    # 1. Plain text becomes the message body.\n",[165,1695,1696],{"class":167,"line":662},[165,1697,1698],{"class":202},"    msg.set_content(text_body(summary, period))\n",[165,1700,1701],{"class":167,"line":668},[165,1702,279],{"emptyLinePlaceholder":278},[165,1704,1705],{"class":167,"line":714},[165,1706,1707],{"class":1692},"    # 2. A Content-ID for the chart, if there is one.\n",[165,1709,1710,1713,1715],{"class":167,"line":720},[165,1711,1712],{"class":202},"    cid ",[165,1714,206],{"class":198},[165,1716,1717],{"class":222}," None\n",[165,1719,1720,1723],{"class":167,"line":742},[165,1721,1722],{"class":198},"    if",[165,1724,1725],{"class":202}," chart_path:\n",[165,1727,1728,1731,1733,1736,1739,1741,1744],{"class":167,"line":1063},[165,1729,1730],{"class":202},"        cid ",[165,1732,206],{"class":198},[165,1734,1735],{"class":202}," make_msgid(",[165,1737,1738],{"class":651},"domain",[165,1740,206],{"class":198},[165,1742,1743],{"class":175},"\"reports.example.com\"",[165,1745,811],{"class":202},[165,1747,1748],{"class":167,"line":1081},[165,1749,279],{"emptyLinePlaceholder":278},[165,1751,1752],{"class":167,"line":1089},[165,1753,1754],{"class":1692},"    # 3. HTML is added as an alternative to the text.\n",[165,1756,1757],{"class":167,"line":1094},[165,1758,1759],{"class":202},"    msg.add_alternative(\n",[165,1761,1762,1765,1767,1769,1772,1775,1778,1781,1784,1786,1789],{"class":167,"line":1099},[165,1763,1764],{"class":202},"        html_body(summary, period, ",[165,1766,1120],{"class":651},[165,1768,206],{"class":198},[165,1770,1771],{"class":202},"cid.strip(",[165,1773,1774],{"class":175},"\"\u003C>\"",[165,1776,1777],{"class":202},") ",[165,1779,1780],{"class":198},"if",[165,1782,1783],{"class":202}," cid ",[165,1785,1153],{"class":198},[165,1787,1788],{"class":222}," None",[165,1790,1791],{"class":202},"),\n",[165,1793,1794,1797,1799,1802],{"class":167,"line":1110},[165,1795,1796],{"class":651},"        subtype",[165,1798,206],{"class":198},[165,1800,1801],{"class":175},"\"html\"",[165,1803,539],{"class":202},[165,1805,1806],{"class":167,"line":1128},[165,1807,1162],{"class":202},[165,1809,1810],{"class":167,"line":1136},[165,1811,279],{"emptyLinePlaceholder":278},[165,1813,1814],{"class":167,"line":1144},[165,1815,1816],{"class":1692},"    # 4. The chart is related to the HTML part, not to the message.\n",[165,1818,1819,1821],{"class":167,"line":1159},[165,1820,1722],{"class":198},[165,1822,1725],{"class":202},[165,1824,1825,1828,1830,1833,1836],{"class":167,"line":1165},[165,1826,1827],{"class":202},"        html_part ",[165,1829,206],{"class":198},[165,1831,1832],{"class":202}," msg.get_payload()[",[165,1834,1835],{"class":222},"1",[165,1837,739],{"class":202},[165,1839,1840],{"class":167,"line":1170},[165,1841,1842],{"class":202},"        html_part.add_related(\n",[165,1844,1845],{"class":167,"line":1184},[165,1846,1847],{"class":202},"            Path(chart_path).read_bytes(),\n",[165,1849,1850,1853,1855,1858,1860,1863,1865,1868,1870,1873,1875],{"class":167,"line":1190},[165,1851,1852],{"class":651},"            maintype",[165,1854,206],{"class":198},[165,1856,1857],{"class":175},"\"image\"",[165,1859,303],{"class":202},[165,1861,1862],{"class":651},"subtype",[165,1864,206],{"class":198},[165,1866,1867],{"class":175},"\"png\"",[165,1869,303],{"class":202},[165,1871,1872],{"class":651},"cid",[165,1874,206],{"class":198},[165,1876,1877],{"class":202},"cid,\n",[165,1879,1880,1883,1885],{"class":167,"line":1196},[165,1881,1882],{"class":651},"            filename",[165,1884,206],{"class":198},[165,1886,1887],{"class":202},"Path(chart_path).name,\n",[165,1889,1890],{"class":167,"line":1202},[165,1891,717],{"class":202},[165,1893,1894],{"class":167,"line":1208},[165,1895,279],{"emptyLinePlaceholder":278},[165,1897,1898],{"class":167,"line":1214},[165,1899,1900],{"class":1692},"    # 5. The workbook attaches to the message itself.\n",[165,1902,1903,1906,1908],{"class":167,"line":1220},[165,1904,1905],{"class":202},"    data ",[165,1907,206],{"class":198},[165,1909,1910],{"class":202}," Path(attachment).read_bytes()\n",[165,1912,1913,1916,1919,1921,1924],{"class":167,"line":1226},[165,1914,1915],{"class":202},"    msg.add_attachment(data, ",[165,1917,1918],{"class":651},"maintype",[165,1920,206],{"class":198},[165,1922,1923],{"class":175},"\"application\"",[165,1925,539],{"class":202},[165,1927,1928,1931,1933,1935,1938,1941,1943,1945,1948,1950],{"class":167,"line":1232},[165,1929,1930],{"class":651},"                       subtype",[165,1932,206],{"class":198},[165,1934,1569],{"class":222},[165,1936,1937],{"class":202},".split(",[165,1939,1940],{"class":175},"\"\u002F\"",[165,1942,303],{"class":202},[165,1944,1835],{"class":222},[165,1946,1947],{"class":202},")[",[165,1949,1835],{"class":222},[165,1951,319],{"class":202},[165,1953,1954,1957,1959],{"class":167,"line":1238},[165,1955,1956],{"class":651},"                       filename",[165,1958,206],{"class":198},[165,1960,1961],{"class":202},"Path(attachment).name)\n",[165,1963,1964,1966],{"class":167,"line":1244},[165,1965,745],{"class":198},[165,1967,1968],{"class":202}," msg\n",[10,1970,1971,1972,1975,1976,1979,1980,1982,1983,1986,1987,1990,1991,1994,1995,1998],{},"The order is not cosmetic. ",[14,1973,1974],{},"set_content"," before ",[14,1977,1978],{},"add_alternative"," produces a ",[14,1981,81],{},"; the other way round leaves the text as an attachment. And ",[14,1984,1985],{},"add_related"," must be called on the ",[1482,1988,1989],{},"HTML part",", not the message — a ",[14,1992,1993],{},"cid:"," reference only resolves within the ",[14,1996,1997],{},"multipart\u002Frelated"," container holding that HTML.",[10,2000,2001,2002,2005,2006,2009,2010,2013],{},"The ",[14,2003,2004],{},"cid.strip(\"\u003C>\")"," is easy to miss: ",[14,2007,2008],{},"make_msgid"," returns an angle-bracketed identifier for the header, while the ",[14,2011,2012],{},"src=\"cid:...\""," attribute uses the bare form.",[151,2015,2017],{"id":2016},"step-4-send-it","Step 4 — Send it",[25,2019,35,2025,35,2028,35,2031,35,2034,35,2039,35,2043,35,2048,35,2052,35,2056,35,2059,35,2063,35,2065,35,2070,35,2073,35,2076,35,2079,35,2082],{"viewBox":2020,"role":28,"ariaLabel":2021,"ariaLabelledBy":2022,"xmlns":33,"style":34},"0 0 800 224","CSS support in mail clients: inline styles and table layout are reliable, while stylesheets, flexbox and grid are stripped or ignored.",[2023,2024],"css-t","css-d",[37,2026,2027],{"id":2023},"What survives in a mail client, and what does not",[41,2029,2030],{"id":2024},"Two columns. Reliable across clients: inline style attributes, table-based layout, and the basic properties of colour, background colour, padding, border and font. Unreliable or stripped entirely: style blocks and external stylesheets, flexbox and grid layout, CSS positioning, and background images. Designing for the reliable column produces an email that looks the same everywhere rather than one that looks best in a browser preview.",[45,2032],{"x":47,"y":47,"width":48,"height":2033,"fill":50},"224",[45,2035],{"x":53,"y":2036,"width":2037,"height":2038,"rx":53,"fill":101,"stroke":102,"style":59},"22","368","184",[61,2040,2042],{"x":110,"y":64,"style":2041},"font-size:12.5px;font-weight:700;fill:var(--teal-ink,#0b6157);text-anchor:middle","reliable everywhere",[61,2044,2047],{"x":110,"y":2045,"style":2046},"82","font-size:11.5px;fill:var(--text,#172033);text-anchor:middle","inline style attributes",[61,2049,2051],{"x":110,"y":2050,"style":2046},"108","table-based layout",[61,2053,2055],{"x":110,"y":2054,"style":2046},"134","color, background-color, padding",[61,2057,2058],{"x":110,"y":72,"style":2046},"border, font-size, font-weight",[61,2060,2062],{"x":110,"y":2061,"style":106},"190","design for this column",[45,2064],{"x":71,"y":2036,"width":2037,"height":2038,"rx":53,"fill":136,"stroke":137,"style":59},[61,2066,2069],{"x":2067,"y":64,"style":2068},"602","font-size:12.5px;font-weight:700;fill:var(--accent-ink,#be185d);text-anchor:middle","stripped or ignored",[61,2071,2072],{"x":2067,"y":2045,"style":2046},"style blocks and stylesheets",[61,2074,2075],{"x":2067,"y":2050,"style":2046},"flexbox and grid",[61,2077,2078],{"x":2067,"y":2054,"style":2046},"position and float tricks",[61,2080,2081],{"x":2067,"y":72,"style":2046},"background images",[61,2083,2085],{"x":2067,"y":2061,"style":2084},"font-size:11px;font-weight:700;fill:var(--accent-ink,#be185d);text-anchor:middle","a browser preview will not warn you",[156,2087,2089],{"className":255,"code":2088,"language":257,"meta":161,"style":161},"import os\nimport smtplib\n\ndef send(msg, host=None, port=None, user=None, password=None):\n    host = host or os.environ[\"SMTP_HOST\"]\n    port = int(port or os.environ.get(\"SMTP_PORT\", 587))\n    user = user or os.environ[\"SMTP_USER\"]\n    password = password or os.environ[\"SMTP_PASSWORD\"]\n\n    with smtplib.SMTP(host, port, timeout=30) as smtp:\n        smtp.starttls()\n        smtp.login(user, password)\n        smtp.send_message(msg)\n    return True\n",[14,2090,2091,2098,2105,2109,2146,2167,2196,2215,2234,2238,2261,2266,2271,2276],{"__ignoreMap":161},[165,2092,2093,2095],{"class":167,"line":168},[165,2094,264],{"class":198},[165,2096,2097],{"class":202}," os\n",[165,2099,2100,2102],{"class":167,"line":212},[165,2101,264],{"class":198},[165,2103,2104],{"class":202}," smtplib\n",[165,2106,2107],{"class":167,"line":226},[165,2108,279],{"emptyLinePlaceholder":278},[165,2110,2111,2113,2116,2119,2121,2123,2126,2128,2130,2133,2135,2137,2140,2142,2144],{"class":167,"line":239},[165,2112,435],{"class":198},[165,2114,2115],{"class":438}," send",[165,2117,2118],{"class":202},"(msg, host",[165,2120,206],{"class":198},[165,2122,836],{"class":222},[165,2124,2125],{"class":202},", port",[165,2127,206],{"class":198},[165,2129,836],{"class":222},[165,2131,2132],{"class":202},", user",[165,2134,206],{"class":198},[165,2136,836],{"class":222},[165,2138,2139],{"class":202},", password",[165,2141,206],{"class":198},[165,2143,836],{"class":222},[165,2145,450],{"class":202},[165,2147,2148,2151,2153,2156,2159,2162,2165],{"class":167,"line":322},[165,2149,2150],{"class":202},"    host ",[165,2152,206],{"class":198},[165,2154,2155],{"class":202}," host ",[165,2157,2158],{"class":198},"or",[165,2160,2161],{"class":202}," os.environ[",[165,2163,2164],{"class":175},"\"SMTP_HOST\"",[165,2166,739],{"class":202},[165,2168,2169,2172,2174,2177,2180,2182,2185,2188,2190,2193],{"class":167,"line":350},[165,2170,2171],{"class":202},"    port ",[165,2173,206],{"class":198},[165,2175,2176],{"class":222}," int",[165,2178,2179],{"class":202},"(port ",[165,2181,2158],{"class":198},[165,2183,2184],{"class":202}," os.environ.get(",[165,2186,2187],{"class":175},"\"SMTP_PORT\"",[165,2189,303],{"class":202},[165,2191,2192],{"class":222},"587",[165,2194,2195],{"class":202},"))\n",[165,2197,2198,2201,2203,2206,2208,2210,2213],{"class":167,"line":377},[165,2199,2200],{"class":202},"    user ",[165,2202,206],{"class":198},[165,2204,2205],{"class":202}," user ",[165,2207,2158],{"class":198},[165,2209,2161],{"class":202},[165,2211,2212],{"class":175},"\"SMTP_USER\"",[165,2214,739],{"class":202},[165,2216,2217,2220,2222,2225,2227,2229,2232],{"class":167,"line":383},[165,2218,2219],{"class":202},"    password ",[165,2221,206],{"class":198},[165,2223,2224],{"class":202}," password ",[165,2226,2158],{"class":198},[165,2228,2161],{"class":202},[165,2230,2231],{"class":175},"\"SMTP_PASSWORD\"",[165,2233,739],{"class":202},[165,2235,2236],{"class":167,"line":542},[165,2237,279],{"emptyLinePlaceholder":278},[165,2239,2240,2243,2246,2249,2251,2254,2256,2258],{"class":167,"line":550},[165,2241,2242],{"class":198},"    with",[165,2244,2245],{"class":202}," smtplib.SMTP(host, port, ",[165,2247,2248],{"class":651},"timeout",[165,2250,206],{"class":198},[165,2252,2253],{"class":222},"30",[165,2255,1777],{"class":202},[165,2257,270],{"class":198},[165,2259,2260],{"class":202}," smtp:\n",[165,2262,2263],{"class":167,"line":572},[165,2264,2265],{"class":202},"        smtp.starttls()\n",[165,2267,2268],{"class":167,"line":593},[165,2269,2270],{"class":202},"        smtp.login(user, password)\n",[165,2272,2273],{"class":167,"line":615},[165,2274,2275],{"class":202},"        smtp.send_message(msg)\n",[165,2277,2278,2280],{"class":167,"line":622},[165,2279,745],{"class":198},[165,2281,2282],{"class":222}," True\n",[10,2284,2285],{},"Preview before sending — reviewing the rendered HTML in a browser catches layout problems far faster than sending yourself test messages:",[156,2287,2289],{"className":255,"code":2288,"language":257,"meta":161,"style":161},"from pathlib import Path\n\nPath(\"preview.html\").write_text(html_body(summary), encoding=\"utf-8\")\nprint(\"open preview.html in a browser\")\n",[14,2290,2291,2301,2305,2326],{"__ignoreMap":161},[165,2292,2293,2295,2297,2299],{"class":167,"line":168},[165,2294,1528],{"class":198},[165,2296,1555],{"class":202},[165,2298,264],{"class":198},[165,2300,1560],{"class":202},[165,2302,2303],{"class":167,"line":212},[165,2304,279],{"emptyLinePlaceholder":278},[165,2306,2307,2310,2313,2316,2319,2321,2324],{"class":167,"line":226},[165,2308,2309],{"class":202},"Path(",[165,2311,2312],{"class":175},"\"preview.html\"",[165,2314,2315],{"class":202},").write_text(html_body(summary), ",[165,2317,2318],{"class":651},"encoding",[165,2320,206],{"class":198},[165,2322,2323],{"class":175},"\"utf-8\"",[165,2325,811],{"class":202},[165,2327,2328,2331,2334,2337],{"class":167,"line":239},[165,2329,2330],{"class":222},"print",[165,2332,2333],{"class":202},"(",[165,2335,2336],{"class":175},"\"open preview.html in a browser\"",[165,2338,811],{"class":202},[10,2340,2341],{},"A browser preview is optimistic, because browsers support far more CSS than mail clients do. Treat it as a first check, then send one real test to the clients your audience actually uses.",[151,2343,2345],{"id":2344},"step-5-keep-the-message-small","Step 5 — Keep the message small",[10,2347,2348],{},"Mail servers reject large messages, and base64 encoding inflates every attachment by about a third.",[25,2350,35,2356,35,2359,35,2362,35,2365,35,2370,35,2374,35,2380,35,2384,35,2388,35,2391,35,2394,35,2397,35,2402,35,2408,35,2413],{"viewBox":2351,"role":28,"ariaLabel":2352,"ariaLabelledBy":2353,"xmlns":33,"style":34},"0 0 800 240","Message size budget: a 12 megabyte workbook becomes 16 megabytes after base64 encoding and exceeds a typical limit, whereas linking the workbook and embedding only a small chart stays well within it.",[2354,2355],"size-t","size-d",[37,2357,2358],{"id":2354},"Base64 encoding inflates every attachment by a third",[41,2360,2361],{"id":2355},"Two message compositions against a typical twenty-five megabyte limit. Attaching a twelve megabyte workbook plus a chart produces roughly sixteen megabytes on the wire once base64 encoding is applied, which is close enough to the limit that some gateways reject it. Attaching only a small chart image and linking the workbook keeps the message under one megabyte and never bounces.",[45,2363],{"x":47,"y":47,"width":48,"height":2364,"fill":50},"240",[61,2366,2369],{"x":63,"y":2367,"style":2368},"28","font-size:12px;font-weight:700;fill:var(--muted,#5b6780);text-anchor:middle","size on the wire, against a typical 25 MB limit",[45,2371],{"x":53,"y":2372,"width":135,"height":2253,"rx":2373,"fill":57,"stroke":58},"52","6",[61,2375,2379],{"x":2376,"y":2377,"style":2378},"89","72","font-size:10.5px;font-weight:700;fill:var(--text,#172033);text-anchor:middle","attach the workbook",[45,2381],{"x":105,"y":2372,"width":2382,"height":2253,"rx":2383,"fill":136,"stroke":137,"style":59},"512","5",[61,2385,2387],{"x":2386,"y":2377,"style":2084},"436","12 MB file → ~16 MB encoded",[45,2389],{"x":53,"y":2390,"width":135,"height":2253,"rx":2373,"fill":57,"stroke":58},"122",[61,2392,2393],{"x":2376,"y":96,"style":2378},"link it instead",[45,2395],{"x":105,"y":2390,"width":2396,"height":2253,"rx":2383,"fill":101,"stroke":102,"style":59},"34",[61,2398,2401],{"x":2399,"y":96,"style":2400},"226","font-size:11px;font-weight:700;fill:var(--teal-ink,#0b6157)","under 1 MB — the chart and the body only",[167,2403],{"x1":2404,"y1":2405,"x2":2404,"y2":2406,"stroke":137,"style":2407},"700","44","164","stroke-width:2.5px;stroke-dasharray:5 4",[61,2409,2412],{"x":2410,"y":105,"style":2411},"694","font-size:10px;font-weight:700;fill:var(--accent-ink,#be185d);text-anchor:end","25 MB limit",[61,2414,2417],{"x":63,"y":2415,"style":2416},"212","font-size:11px;fill:var(--muted,#5b6780);text-anchor:middle","the limit applies to the encoded size, and the receiving server's limit may be lower than yours",[156,2419,2421],{"className":255,"code":2420,"language":257,"meta":161,"style":161},"from pathlib import Path\n\nMAX_ENCODED_MB = 20\n\ndef check_size(*paths, limit_mb=MAX_ENCODED_MB):\n    \"\"\"Refuse to build a message whose attachments will be rejected.\"\"\"\n    raw = sum(Path(p).stat().st_size for p in paths)\n    encoded_mb = raw * 4 \u002F 3 \u002F 1024 \u002F 1024\n    if encoded_mb > limit_mb:\n        raise ValueError(\n            f\"attachments encode to about {encoded_mb:.1f} MB, over the \"\n            f\"{limit_mb} MB budget — publish the workbook and send a link.\"\n        )\n    return encoded_mb\n",[14,2422,2423,2433,2437,2447,2451,2472,2477,2501,2532,2545,2556,2576,2592,2596],{"__ignoreMap":161},[165,2424,2425,2427,2429,2431],{"class":167,"line":168},[165,2426,1528],{"class":198},[165,2428,1555],{"class":202},[165,2430,264],{"class":198},[165,2432,1560],{"class":202},[165,2434,2435],{"class":167,"line":212},[165,2436,279],{"emptyLinePlaceholder":278},[165,2438,2439,2442,2444],{"class":167,"line":226},[165,2440,2441],{"class":222},"MAX_ENCODED_MB",[165,2443,787],{"class":198},[165,2445,2446],{"class":222}," 20\n",[165,2448,2449],{"class":167,"line":239},[165,2450,279],{"emptyLinePlaceholder":278},[165,2452,2453,2455,2458,2460,2463,2466,2468,2470],{"class":167,"line":322},[165,2454,435],{"class":198},[165,2456,2457],{"class":438}," check_size",[165,2459,2333],{"class":202},[165,2461,2462],{"class":198},"*",[165,2464,2465],{"class":202},"paths, limit_mb",[165,2467,206],{"class":198},[165,2469,2441],{"class":222},[165,2471,450],{"class":202},[165,2473,2474],{"class":167,"line":350},[165,2475,2476],{"class":175},"    \"\"\"Refuse to build a message whose attachments will be rejected.\"\"\"\n",[165,2478,2479,2482,2484,2487,2490,2493,2496,2498],{"class":167,"line":377},[165,2480,2481],{"class":202},"    raw ",[165,2483,206],{"class":198},[165,2485,2486],{"class":222}," sum",[165,2488,2489],{"class":202},"(Path(p).stat().st_size ",[165,2491,2492],{"class":198},"for",[165,2494,2495],{"class":202}," p ",[165,2497,645],{"class":198},[165,2499,2500],{"class":202}," paths)\n",[165,2502,2503,2506,2508,2511,2513,2516,2519,2522,2524,2527,2529],{"class":167,"line":383},[165,2504,2505],{"class":202},"    encoded_mb ",[165,2507,206],{"class":198},[165,2509,2510],{"class":202}," raw ",[165,2512,2462],{"class":198},[165,2514,2515],{"class":222}," 4",[165,2517,2518],{"class":198}," \u002F",[165,2520,2521],{"class":222}," 3",[165,2523,2518],{"class":198},[165,2525,2526],{"class":222}," 1024",[165,2528,2518],{"class":198},[165,2530,2531],{"class":222}," 1024\n",[165,2533,2534,2536,2539,2542],{"class":167,"line":542},[165,2535,1722],{"class":198},[165,2537,2538],{"class":202}," encoded_mb ",[165,2540,2541],{"class":198},">",[165,2543,2544],{"class":202}," limit_mb:\n",[165,2546,2547,2550,2553],{"class":167,"line":550},[165,2548,2549],{"class":198},"        raise",[165,2551,2552],{"class":222}," ValueError",[165,2554,2555],{"class":202},"(\n",[165,2557,2558,2560,2563,2565,2568,2571,2573],{"class":167,"line":572},[165,2559,671],{"class":198},[165,2561,2562],{"class":175},"\"attachments encode to about ",[165,2564,527],{"class":526},[165,2566,2567],{"class":202},"encoded_mb",[165,2569,2570],{"class":198},":.1f",[165,2572,533],{"class":526},[165,2574,2575],{"class":175}," MB, over the \"\n",[165,2577,2578,2580,2582,2584,2587,2589],{"class":167,"line":593},[165,2579,671],{"class":198},[165,2581,536],{"class":175},[165,2583,527],{"class":526},[165,2585,2586],{"class":202},"limit_mb",[165,2588,533],{"class":526},[165,2590,2591],{"class":175}," MB budget — publish the workbook and send a link.\"\n",[165,2593,2594],{"class":167,"line":615},[165,2595,717],{"class":202},[165,2597,2598,2600],{"class":167,"line":622},[165,2599,745],{"class":198},[165,2601,2602],{"class":202}," encoded_mb\n",[10,2604,2605,2606,2610],{},"When the workbook is large, publish it and link instead — the body still carries the figures, so the email is complete on its own. That is the pattern described in ",[19,2607,2609],{"href":2608},"\u002Fautomating-reporting-workflows\u002Fpublishing-excel-reports-to-cloud-storage\u002F","publishing Excel reports to cloud storage",", and the summary body is what makes a link-only email useful rather than an extra click.",[151,2612,2614],{"id":2613},"common-pitfalls-and-fixes","Common pitfalls and fixes",[2616,2617,2618,2634],"table",{},[2619,2620,2621],"thead",{},[2622,2623,2624,2628,2631],"tr",{},[2625,2626,2627],"th",{},"Symptom",[2625,2629,2630],{},"Cause",[2625,2632,2633],{},"Fix",[2635,2636,2637,2649,2660,2677,2690,2713,2726,2737],"tbody",{},[2622,2638,2639,2643,2646],{},[2640,2641,2642],"td",{},"CSS ignored in Outlook",[2640,2644,2645],{},"Stylesheets stripped",[2640,2647,2648],{},"Put every style inline.",[2622,2650,2651,2654,2657],{},[2640,2652,2653],{},"Layout collapses",[2640,2655,2656],{},"Flexbox or grid used",[2640,2658,2659],{},"Lay out with tables.",[2622,2661,2662,2665,2670],{},[2640,2663,2664],{},"Text part arrives as an attachment",[2640,2666,2667,2669],{},[14,2668,1978],{}," called first",[2640,2671,2672,2674,2675,23],{},[14,2673,1974],{}," first, then ",[14,2676,1978],{},[2622,2678,2679,2682,2687],{},[2640,2680,2681],{},"Embedded image does not show",[2640,2683,2684,2686],{},[14,2685,1985],{}," on the message",[2640,2688,2689],{},"Call it on the HTML part.",[2622,2691,2692,2695,2700],{},[2640,2693,2694],{},"Broken image icon",[2640,2696,2697,2698],{},"Angle brackets left in the ",[14,2699,1993],{},[2640,2701,2702,2703,2706,2707,2709,2710,23],{},"Strip ",[14,2704,2705],{},"\u003C"," and ",[14,2708,2541],{}," in the ",[14,2711,2712],{},"src",[2622,2714,2715,2718,2721],{},[2640,2716,2717],{},"Markup broken by a value",[2640,2719,2720],{},"Data not escaped",[2640,2722,2723,2725],{},[14,2724,1492],{}," every interpolated value.",[2622,2727,2728,2731,2734],{},[2640,2729,2730],{},"Message rejected as too large",[2640,2732,2733],{},"Base64 inflates by a third",[2640,2735,2736],{},"Link the workbook instead.",[2622,2738,2739,2742,2745],{},[2640,2740,2741],{},"Flagged as spam",[2640,2743,2744],{},"HTML-only message",[2640,2746,2747],{},"Always include a text part.",[151,2749,2751],{"id":2750},"performance-and-scale-notes","Performance and scale notes",[10,2753,2754],{},"Building the message is trivial; the costs are the SMTP connection and the encoding.",[10,2756,2757,2760],{},[1482,2758,2759],{},"Reuse one connection for a batch."," Opening and authenticating a session per recipient is the single biggest waste in a fan-out job:",[156,2762,2764],{"className":255,"code":2763,"language":257,"meta":161,"style":161},"import os\nimport smtplib\n\ndef send_batch(messages):\n    \"\"\"One connection, many messages.\"\"\"\n    with smtplib.SMTP(os.environ[\"SMTP_HOST\"],\n                      int(os.environ.get(\"SMTP_PORT\", 587)), timeout=30) as smtp:\n        smtp.starttls()\n        smtp.login(os.environ[\"SMTP_USER\"], os.environ[\"SMTP_PASSWORD\"])\n        for msg in messages:\n            smtp.send_message(msg)\n",[14,2765,2766,2772,2778,2782,2792,2797,2808,2837,2841,2856,2869],{"__ignoreMap":161},[165,2767,2768,2770],{"class":167,"line":168},[165,2769,264],{"class":198},[165,2771,2097],{"class":202},[165,2773,2774,2776],{"class":167,"line":212},[165,2775,264],{"class":198},[165,2777,2104],{"class":202},[165,2779,2780],{"class":167,"line":226},[165,2781,279],{"emptyLinePlaceholder":278},[165,2783,2784,2786,2789],{"class":167,"line":239},[165,2785,435],{"class":198},[165,2787,2788],{"class":438}," send_batch",[165,2790,2791],{"class":202},"(messages):\n",[165,2793,2794],{"class":167,"line":322},[165,2795,2796],{"class":175},"    \"\"\"One connection, many messages.\"\"\"\n",[165,2798,2799,2801,2804,2806],{"class":167,"line":350},[165,2800,2242],{"class":198},[165,2802,2803],{"class":202}," smtplib.SMTP(os.environ[",[165,2805,2164],{"class":175},[165,2807,319],{"class":202},[165,2809,2810,2813,2816,2818,2820,2822,2825,2827,2829,2831,2833,2835],{"class":167,"line":377},[165,2811,2812],{"class":222},"                      int",[165,2814,2815],{"class":202},"(os.environ.get(",[165,2817,2187],{"class":175},[165,2819,303],{"class":202},[165,2821,2192],{"class":222},[165,2823,2824],{"class":202},")), ",[165,2826,2248],{"class":651},[165,2828,206],{"class":198},[165,2830,2253],{"class":222},[165,2832,1777],{"class":202},[165,2834,270],{"class":198},[165,2836,2260],{"class":202},[165,2838,2839],{"class":167,"line":383},[165,2840,2265],{"class":202},[165,2842,2843,2846,2848,2851,2853],{"class":167,"line":542},[165,2844,2845],{"class":202},"        smtp.login(os.environ[",[165,2847,2212],{"class":175},[165,2849,2850],{"class":202},"], os.environ[",[165,2852,2231],{"class":175},[165,2854,2855],{"class":202},"])\n",[165,2857,2858,2861,2864,2866],{"class":167,"line":550},[165,2859,2860],{"class":198},"        for",[165,2862,2863],{"class":202}," msg ",[165,2865,645],{"class":198},[165,2867,2868],{"class":202}," messages:\n",[165,2870,2871],{"class":167,"line":572},[165,2872,2873],{"class":202},"            smtp.send_message(msg)\n",[10,2875,2876,2879],{},[1482,2877,2878],{},"Build the attachment bytes once."," Where forty regional emails share one workbook, read it once and reuse the bytes rather than re-reading per message.",[10,2881,2882,2885,2886,2889,2890,2894],{},[1482,2883,2884],{},"Respect the provider's rate limit."," Most services cap messages per minute and per day, and exceeding it gets an account throttled or suspended. Pace the loop and back off on a ",[14,2887,2888],{},"SMTPResponseException",", as in ",[19,2891,2893],{"href":2892},"\u002Fautomating-reporting-workflows\u002Ferror-handling-and-logging-in-excel-automation\u002Fretry-a-failed-excel-report-job-in-python\u002F","retrying a failed Excel report job"," — and never retry an authentication failure, which will fail identically every time.",[10,2896,2897,2898,2902],{},"One design note that outweighs all of the above: for a report going to more than a handful of people, personalising the ",[2899,2900,2901],"em",{},"body"," while linking a single published workbook scales far better than attaching a per-recipient file. The body is cheap to build, the link costs nothing, and the storage holds one copy instead of forty.",[151,2904,2906],{"id":2905},"conclusion","Conclusion",[10,2908,2909],{},"An email whose body carries the headline figures answers most recipients' question without them opening anything. Write the plain-text version first — it keeps the summary honest and keeps the message out of spam filters — then add the HTML as an alternative with every style inline and tables for layout, because mail clients strip stylesheets and ignore modern CSS. Escape the data, embed a chart with a Content-ID related to the HTML part, and attach the workbook for the detail. Then check the encoded size, and when the workbook is large, publish it and send a link instead.",[151,2911,2913],{"id":2912},"frequently-asked-questions","Frequently asked questions",[10,2915,2916,2919,2920,2922],{},[1482,2917,2918],{},"Why do I need a plain-text part as well as HTML?","\nSome clients and most automated processors read the text part, and a message with only HTML is more likely to be treated as spam. ",[14,2921,1510],{}," handles both: set the text content first, then add the HTML as an alternative.",[10,2924,2925,2928,2929,2932],{},[1482,2926,2927],{},"Why is my CSS ignored in Outlook?","\nMail clients strip stylesheets and many CSS properties. Put every style inline on the element with a ",[14,2930,2931],{},"style"," attribute, use tables for layout rather than flexbox or grid, and stick to background-color, color, padding, border and font properties.",[10,2934,2935,2938,2941,2942,2945],{},[1482,2936,2937],{},"How do I render a DataFrame as an HTML table?",[14,2939,2940],{},"DataFrame.to_html"," gives you the markup, but its classes are useless in email. Generate the rows yourself with inline styles, or post-process ",[14,2943,2944],{},"to_html"," output to inject the styles into each tag.",[10,2947,2948,2951],{},[1482,2949,2950],{},"Should the email carry the numbers or just a link?","\nBoth. Put the three or four figures a reader checks in the body so the email answers the question on a phone, and attach or link the workbook for anyone who needs the detail.",[10,2953,2954,2957,2958,2960,2961,2963],{},[1482,2955,2956],{},"How do I embed a chart image in the body?","\nAttach it with a Content-ID and reference it from an ",[14,2959,28],{}," tag with a ",[14,2962,1993],{}," URL. Many clients block remote images by default, so an embedded CID image is far more reliable than a hosted one.",[151,2965,2967],{"id":2966},"related","Related",[2969,2970,2971,2978,2985,2992,2998],"ul",{},[2972,2973,2974,2975,2977],"li",{},"Up to the parent: ",[19,2976,22],{"href":21}," — the connection and attachment basics.",[2972,2979,2980,2984],{},[19,2981,2983],{"href":2982},"\u002Fautomating-reporting-workflows\u002Femailing-excel-reports-with-smtplib\u002Fattach-multiple-excel-files-to-one-email-in-python\u002F","Attach Multiple Excel Files to One Email in Python"," — sending several workbooks at once.",[2972,2986,2987,2991],{},[19,2988,2990],{"href":2989},"\u002Fautomating-reporting-workflows\u002Femailing-excel-reports-with-smtplib\u002Fsend-excel-report-to-multiple-recipients-python\u002F","Send an Excel Report to Multiple Recipients with Python"," — addressing and Bcc handling.",[2972,2993,2994,2997],{},[19,2995,2996],{"href":2608},"Publishing Excel Reports to Cloud Storage"," — linking rather than attaching a large workbook.",[2972,2999,3000,3004],{},[19,3001,3003],{"href":3002},"\u002Fformatting-and-charting-excel-reports-with-python\u002Finserting-images-and-logos-into-excel\u002Fembed-a-matplotlib-chart-in-an-excel-report\u002F","Embed a matplotlib Chart in an Excel Report"," — producing the chart image the body embeds.",[2931,3006,3007],{},"html pre.shiki code .sMTad, html code.shiki .sMTad{--shiki-default:#6F42C1;--shiki-dark:#FFB757}html pre.shiki code .srMev, html code.shiki .srMev{--shiki-default:#032F62;--shiki-dark:#ADDCFF}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .s-kum, html code.shiki .s-kum{--shiki-default:#D73A49;--shiki-dark:#FF9492}html pre.shiki code .skGVy, html code.shiki .skGVy{--shiki-default:#24292E;--shiki-dark:#F0F3F6}html pre.shiki code .sP0c6, html code.shiki .sP0c6{--shiki-default:#005CC5;--shiki-dark:#91CBFF}html pre.shiki code .s_Opv, html code.shiki .s_Opv{--shiki-default:#6F42C1;--shiki-dark:#DBB7FF}html pre.shiki code .sSjpA, html code.shiki .sSjpA{--shiki-default:#005CC5;--shiki-dark:#FF9492}html pre.shiki code .sa561, html code.shiki .sa561{--shiki-default:#E36209;--shiki-dark:#FFB757}html pre.shiki code .s-wDw, html code.shiki .s-wDw{--shiki-default:#6A737D;--shiki-dark:#BDC4CC}",{"title":161,"searchDepth":212,"depth":212,"links":3009},[3010,3011,3012,3013,3014,3015,3016,3017,3018,3019,3020],{"id":153,"depth":212,"text":154},{"id":421,"depth":212,"text":422},{"id":759,"depth":212,"text":760},{"id":1504,"depth":212,"text":1505},{"id":2016,"depth":212,"text":2017},{"id":2344,"depth":212,"text":2345},{"id":2613,"depth":212,"text":2614},{"id":2750,"depth":212,"text":2751},{"id":2905,"depth":212,"text":2906},{"id":2912,"depth":212,"text":2913},{"id":2966,"depth":212,"text":2967},"2026-08-15","Put the headline numbers in the email itself — build an HTML body with a plain-text fallback, render a DataFrame as a styled table, and attach the workbook for the detail.","md",[3025,3029,3031,3033,3035],{"q":2918,"a":3026},{"Some clients and most automated processors read the text part, and a message with only HTML is more likely to be treated as spam":3027},{" EmailMessage handles both":3028},"set the text content first, then add the HTML as an alternative.",{"q":2927,"a":3030},"Mail clients strip stylesheets and many CSS properties. Put every style inline on the element with a style attribute, use tables for layout rather than flexbox or grid, and stick to background-color, color, padding, border and font properties.",{"q":2937,"a":3032},"DataFrame.to_html gives you the markup, but its classes are useless in email. Generate the rows yourself with inline styles, or post-process to_html output to inject the styles into each tag.",{"q":2950,"a":3034},"Both. Put the three or four figures a reader checks in the body so the email answers the question on a phone, and attach or link the workbook for anyone who needs the detail.",{"q":2956,"a":3036},"Attach it with a Content-ID and reference it from an img tag with a cid URL. Many clients block remote images by default, so an embedded CID image is far more reliable than a hosted one.",{},"\u002Fautomating-reporting-workflows\u002Femailing-excel-reports-with-smtplib\u002Femail-an-excel-report-with-an-html-summary-body",{"title":3040,"description":3041},"Email an HTML Summary with an Excel Attachment","Send a report email whose body carries the key figures: EmailMessage with an HTML alternative, a styled DataFrame table, inline CSS for mail clients, and the xlsx attachment.","email-an-excel-report-with-an-html-summary-body","automating-reporting-workflows\u002Femailing-excel-reports-with-smtplib\u002Femail-an-excel-report-with-an-html-summary-body\u002Findex","how-to","lO8X_0Sggdz2i6b1EsIMCxX7-luJyQOx17vvTacikoo",[3047,3050],{"title":2983,"path":3048,"stem":3049,"children":-1},"\u002Fautomating-reporting-workflows\u002Femailing-excel-reports-with-smtplib\u002Fattach-multiple-excel-files-to-one-email-in-python","automating-reporting-workflows\u002Femailing-excel-reports-with-smtplib\u002Fattach-multiple-excel-files-to-one-email-in-python\u002Findex",{"title":3051,"path":3052,"stem":3053,"children":-1},"Send an Excel Report to Multiple Recipients in Python","\u002Fautomating-reporting-workflows\u002Femailing-excel-reports-with-smtplib\u002Fsend-excel-report-to-multiple-recipients-python","automating-reporting-workflows\u002Femailing-excel-reports-with-smtplib\u002Fsend-excel-report-to-multiple-recipients-python\u002Findex",1786800028520]