[{"data":1,"prerenderedAt":2905},["ShallowReactive",2],{"doc:\u002Fautomating-reporting-workflows\u002Femailing-excel-reports-with-smtplib\u002Fattach-multiple-excel-files-to-one-email-in-python":3,"surround:\u002Fautomating-reporting-workflows\u002Femailing-excel-reports-with-smtplib\u002Fattach-multiple-excel-files-to-one-email-in-python":2898},{"id":4,"title":5,"body":6,"dateModified":2875,"datePublished":2875,"description":2876,"extension":2877,"faq":2878,"meta":2889,"navigation":269,"path":2890,"seo":2891,"slug":2894,"stem":2895,"type":2896,"__hash__":2897},"docs\u002Fautomating-reporting-workflows\u002Femailing-excel-reports-with-smtplib\u002Fattach-multiple-excel-files-to-one-email-in-python\u002Findex.md","Attach Multiple Excel Files to One Email in Python",{"type":7,"value":8,"toc":2862},"minimark",[9,24,191,196,224,227,406,410,416,629,640,643,883,886,890,898,1145,1148,1152,1155,1334,1344,1351,1355,1358,1457,1937,1940,1944,2033,2036,2276,2282,2378,2382,2498,2502,2505,2512,2559,2571,2755,2771,2775,2781,2785,2791,2797,2803,2812,2818,2822,2858],[10,11,12,13,17,18,23],"p",{},"One email, several workbooks: a report per region, a summary plus its detail, a month's worth of daily files. ",[14,15,16],"code",{},"EmailMessage"," handles it with a loop — and then the practical questions arrive. What MIME type does each file need, how large can the message be before a gateway rejects it, and how do you know all of them actually made it in? This guide covers the loop and the guard rails around it. 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,52,35,59,35,69,35,74,35,77,35,81,35,86,35,91,35,96,35,100,35,119,35,124,35,131,35,137,35,142,35,150,35,159,35,166,35,172,35,177,35,182,35,187],"svg",{"viewBox":27,"role":28,"ariaLabel":29,"ariaLabelledBy":30,"xmlns":33,"style":34},"4 1 796 205","img","Several files attached to one message, each carrying its own MIME type, with the combined encoded size checked against the gateway limit before sending.",[31,32],"multi-t","multi-d","http:\u002F\u002Fwww.w3.org\u002F2000\u002Fsvg","width:100%;max-width:796px;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},"Four files, four MIME types, one size budget",[41,42,43],"desc",{"id":32},"Four files of different kinds are attached to a single message. Each carries the MIME type its extension implies: spreadsheetml for the two xlsx workbooks, application pdf for the summary, and text csv for the extract. Before sending, the combined size is multiplied by four thirds to account for base64 encoding and checked against the gateway limit, so an over-large message fails locally with a clear message rather than bouncing.",[45,46],"rect",{"x":47,"y":48,"width":49,"height":50,"fill":51},"4","1","796","205","#ffffff",[53,54,58],"text",{"x":55,"y":56,"style":57},"170","28","font-size:11.5px;font-weight:700;fill:var(--muted,#5b6780);text-anchor:middle","the files",[45,60],{"x":61,"y":62,"width":63,"height":64,"rx":65,"fill":66,"stroke":67,"style":68},"20","42","300","30","6","#ebebfd","var(--brand,#5b5cf0)","stroke-width:2px",[53,70,73],{"x":55,"y":71,"style":72},"62","font-size:10.5px;font-weight:700;fill:var(--brand-strong,#4338ca);text-anchor:middle","north-2026-08.xlsx · spreadsheetml",[45,75],{"x":61,"y":76,"width":63,"height":64,"rx":65,"fill":66,"stroke":67,"style":68},"78",[53,78,80],{"x":55,"y":79,"style":72},"98","south-2026-08.xlsx · spreadsheetml",[45,82],{"x":61,"y":83,"width":63,"height":64,"rx":65,"fill":84,"stroke":85,"style":68},"114","#fdefd8","var(--gold,#b4740a)",[53,87,90],{"x":55,"y":88,"style":89},"134","font-size:10.5px;font-weight:700;fill:var(--gold-ink,#7a4e06);text-anchor:middle","summary.pdf · application\u002Fpdf",[45,92],{"x":61,"y":93,"width":63,"height":64,"rx":65,"fill":94,"stroke":95,"style":68},"150","#d9f4f1","var(--teal,#0f9488)",[53,97,99],{"x":55,"y":55,"style":98},"font-size:10.5px;font-weight:700;fill:var(--teal-ink,#0b6157);text-anchor:middle","extract.csv · text\u002Fcsv",[101,102,105,106,105,110,105,113,105,116,35],"g",{"stroke":103,"style":68,"fill":104},"var(--line,#cdd5e6)","none","\n    ",[107,108],"path",{"d":109},"M320 57 H 350 V 111 H 382",[107,111],{"d":112},"M320 93 H 350 V 111",[107,114],{"d":115},"M320 129 H 350 V 111",[107,117],{"d":118},"M320 165 H 350 V 111",[120,121],"polygon",{"points":122,"fill":123},"390,111 378,105 378,117","#5b5cf0",[45,125],{"x":126,"y":127,"width":128,"height":129,"rx":130,"fill":84,"stroke":85,"style":68},"398","76","186","70","12",[53,132,136],{"x":133,"y":134,"style":135},"491","104","font-size:11.5px;font-weight:700;fill:var(--gold-ink,#7a4e06);text-anchor:middle","size × 4\u002F3",[53,138,141],{"x":133,"y":139,"style":140},"126","font-size:10.5px;fill:var(--muted,#5b6780);text-anchor:middle","base64 overhead",[101,143,105,144,105,147,35],{"stroke":85,"style":68,"fill":104},[107,145],{"d":146},"M584 96 H 616 V 62 H 648",[107,148],{"d":149},"M584 126 H 616 V 166 H 648",[101,151,105,153,105,156,35],{"fill":152},"#b4740a",[120,154],{"points":155},"656,62 644,56 644,68",[120,157],{"points":158},"656,166 644,160 644,172",[45,160],{"x":161,"y":162,"width":163,"height":164,"rx":165,"fill":94,"stroke":95,"style":68},"664","38","120","48","11",[53,167,171],{"x":168,"y":169,"style":170},"724","60","font-size:11px;font-weight:700;fill:var(--teal-ink,#0b6157);text-anchor:middle","under limit",[53,173,176],{"x":168,"y":174,"style":175},"77","font-size:10px;fill:var(--muted,#5b6780);text-anchor:middle","send",[45,178],{"x":161,"y":179,"width":163,"height":164,"rx":165,"fill":180,"stroke":181,"style":68},"142","#fee8f2","var(--accent,#f43f8f)",[53,183,186],{"x":168,"y":184,"style":185},"164","font-size:11px;font-weight:700;fill:var(--accent-ink,#be185d);text-anchor:middle","over limit",[53,188,190],{"x":168,"y":189,"style":175},"181","zip or link",[192,193,195],"h2",{"id":194},"prerequisites","Prerequisites",[197,198,203],"pre",{"className":199,"code":200,"language":201,"meta":202,"style":202},"language-bash shiki shiki-themes github-light github-dark-high-contrast","pip install pandas xlsxwriter\n","bash","",[14,204,205],{"__ignoreMap":202},[206,207,210,214,218,221],"span",{"class":208,"line":209},"line",1,[206,211,213],{"class":212},"sMTad","pip",[206,215,217],{"class":216},"srMev"," install",[206,219,220],{"class":216}," pandas",[206,222,223],{"class":216}," xlsxwriter\n",[10,225,226],{},"Everything else is standard library. Some files to send:",[197,228,232],{"className":229,"code":230,"language":231,"meta":202,"style":202},"language-python shiki shiki-themes github-light github-dark-high-contrast","from pathlib import Path\nimport pandas as pd\n\nPath(\"out\").mkdir(exist_ok=True)\nfor region in (\"north\", \"south\", \"west\"):\n    pd.DataFrame({\"metric\": [\"revenue\", \"units\"], \"value\": [5150.0, 412]}).to_excel(\n        f\"out\u002F{region}-2026-08.xlsx\", index=False\n    )\n","python",[14,233,234,250,264,271,297,329,368,400],{"__ignoreMap":202},[206,235,236,240,244,247],{"class":208,"line":209},[206,237,239],{"class":238},"s-kum","from",[206,241,243],{"class":242},"skGVy"," pathlib ",[206,245,246],{"class":238},"import",[206,248,249],{"class":242}," Path\n",[206,251,253,255,258,261],{"class":208,"line":252},2,[206,254,246],{"class":238},[206,256,257],{"class":242}," pandas ",[206,259,260],{"class":238},"as",[206,262,263],{"class":242}," pd\n",[206,265,267],{"class":208,"line":266},3,[206,268,270],{"emptyLinePlaceholder":269},true,"\n",[206,272,274,277,280,283,287,290,294],{"class":208,"line":273},4,[206,275,276],{"class":242},"Path(",[206,278,279],{"class":216},"\"out\"",[206,281,282],{"class":242},").mkdir(",[206,284,286],{"class":285},"sa561","exist_ok",[206,288,289],{"class":238},"=",[206,291,293],{"class":292},"sP0c6","True",[206,295,296],{"class":242},")\n",[206,298,300,303,306,309,312,315,318,321,323,326],{"class":208,"line":299},5,[206,301,302],{"class":238},"for",[206,304,305],{"class":242}," region ",[206,307,308],{"class":238},"in",[206,310,311],{"class":242}," (",[206,313,314],{"class":216},"\"north\"",[206,316,317],{"class":242},", ",[206,319,320],{"class":216},"\"south\"",[206,322,317],{"class":242},[206,324,325],{"class":216},"\"west\"",[206,327,328],{"class":242},"):\n",[206,330,332,335,338,341,344,346,349,352,355,357,360,362,365],{"class":208,"line":331},6,[206,333,334],{"class":242},"    pd.DataFrame({",[206,336,337],{"class":216},"\"metric\"",[206,339,340],{"class":242},": [",[206,342,343],{"class":216},"\"revenue\"",[206,345,317],{"class":242},[206,347,348],{"class":216},"\"units\"",[206,350,351],{"class":242},"], ",[206,353,354],{"class":216},"\"value\"",[206,356,340],{"class":242},[206,358,359],{"class":292},"5150.0",[206,361,317],{"class":242},[206,363,364],{"class":292},"412",[206,366,367],{"class":242},"]}).to_excel(\n",[206,369,371,374,377,381,384,387,390,392,395,397],{"class":208,"line":370},7,[206,372,373],{"class":238},"        f",[206,375,376],{"class":216},"\"out\u002F",[206,378,380],{"class":379},"sSjpA","{",[206,382,383],{"class":242},"region",[206,385,386],{"class":379},"}",[206,388,389],{"class":216},"-2026-08.xlsx\"",[206,391,317],{"class":242},[206,393,394],{"class":285},"index",[206,396,289],{"class":238},[206,398,399],{"class":292},"False\n",[206,401,403],{"class":208,"line":402},8,[206,404,405],{"class":242},"    )\n",[192,407,409],{"id":408},"step-1-attach-with-the-right-mime-type","Step 1 — Attach with the right MIME type",[10,411,412,415],{},[14,413,414],{},"mimetypes"," maps an extension to a type, and a small override table covers the Office formats it does not always know:",[197,417,419],{"className":229,"code":418,"language":231,"meta":202,"style":202},"import mimetypes\nfrom pathlib import Path\n\nOVERRIDES = {\n    \".xlsx\": \"application\u002Fvnd.openxmlformats-officedocument.spreadsheetml.sheet\",\n    \".xlsm\": \"application\u002Fvnd.ms-excel.sheet.macroEnabled.12\",\n    \".xls\": \"application\u002Fvnd.ms-excel\",\n    \".csv\": \"text\u002Fcsv\",\n    \".ods\": \"application\u002Fvnd.oasis.opendocument.spreadsheet\",\n}\n\ndef mime_for(path):\n    \"\"\"Return (maintype, subtype) for a file, from its extension.\"\"\"\n    suffix = Path(path).suffix.lower()\n    guessed = OVERRIDES.get(suffix) or mimetypes.guess_type(str(path))[0]\n    guessed = guessed or \"application\u002Foctet-stream\"\n    maintype, _, subtype = guessed.partition(\"\u002F\")\n    return maintype, subtype\n",[14,420,421,428,438,442,453,467,479,491,503,516,522,527,540,546,557,589,604,620],{"__ignoreMap":202},[206,422,423,425],{"class":208,"line":209},[206,424,246],{"class":238},[206,426,427],{"class":242}," mimetypes\n",[206,429,430,432,434,436],{"class":208,"line":252},[206,431,239],{"class":238},[206,433,243],{"class":242},[206,435,246],{"class":238},[206,437,249],{"class":242},[206,439,440],{"class":208,"line":266},[206,441,270],{"emptyLinePlaceholder":269},[206,443,444,447,450],{"class":208,"line":273},[206,445,446],{"class":292},"OVERRIDES",[206,448,449],{"class":238}," =",[206,451,452],{"class":242}," {\n",[206,454,455,458,461,464],{"class":208,"line":299},[206,456,457],{"class":216},"    \".xlsx\"",[206,459,460],{"class":242},": ",[206,462,463],{"class":216},"\"application\u002Fvnd.openxmlformats-officedocument.spreadsheetml.sheet\"",[206,465,466],{"class":242},",\n",[206,468,469,472,474,477],{"class":208,"line":331},[206,470,471],{"class":216},"    \".xlsm\"",[206,473,460],{"class":242},[206,475,476],{"class":216},"\"application\u002Fvnd.ms-excel.sheet.macroEnabled.12\"",[206,478,466],{"class":242},[206,480,481,484,486,489],{"class":208,"line":370},[206,482,483],{"class":216},"    \".xls\"",[206,485,460],{"class":242},[206,487,488],{"class":216},"\"application\u002Fvnd.ms-excel\"",[206,490,466],{"class":242},[206,492,493,496,498,501],{"class":208,"line":402},[206,494,495],{"class":216},"    \".csv\"",[206,497,460],{"class":242},[206,499,500],{"class":216},"\"text\u002Fcsv\"",[206,502,466],{"class":242},[206,504,506,509,511,514],{"class":208,"line":505},9,[206,507,508],{"class":216},"    \".ods\"",[206,510,460],{"class":242},[206,512,513],{"class":216},"\"application\u002Fvnd.oasis.opendocument.spreadsheet\"",[206,515,466],{"class":242},[206,517,519],{"class":208,"line":518},10,[206,520,521],{"class":242},"}\n",[206,523,525],{"class":208,"line":524},11,[206,526,270],{"emptyLinePlaceholder":269},[206,528,530,533,537],{"class":208,"line":529},12,[206,531,532],{"class":238},"def",[206,534,536],{"class":535},"s_Opv"," mime_for",[206,538,539],{"class":242},"(path):\n",[206,541,543],{"class":208,"line":542},13,[206,544,545],{"class":216},"    \"\"\"Return (maintype, subtype) for a file, from its extension.\"\"\"\n",[206,547,549,552,554],{"class":208,"line":548},14,[206,550,551],{"class":242},"    suffix ",[206,553,289],{"class":238},[206,555,556],{"class":242}," Path(path).suffix.lower()\n",[206,558,560,563,565,568,571,574,577,580,583,586],{"class":208,"line":559},15,[206,561,562],{"class":242},"    guessed ",[206,564,289],{"class":238},[206,566,567],{"class":292}," OVERRIDES",[206,569,570],{"class":242},".get(suffix) ",[206,572,573],{"class":238},"or",[206,575,576],{"class":242}," mimetypes.guess_type(",[206,578,579],{"class":292},"str",[206,581,582],{"class":242},"(path))[",[206,584,585],{"class":292},"0",[206,587,588],{"class":242},"]\n",[206,590,592,594,596,599,601],{"class":208,"line":591},16,[206,593,562],{"class":242},[206,595,289],{"class":238},[206,597,598],{"class":242}," guessed ",[206,600,573],{"class":238},[206,602,603],{"class":216}," \"application\u002Foctet-stream\"\n",[206,605,607,610,612,615,618],{"class":208,"line":606},17,[206,608,609],{"class":242},"    maintype, _, subtype ",[206,611,289],{"class":238},[206,613,614],{"class":242}," guessed.partition(",[206,616,617],{"class":216},"\"\u002F\"",[206,619,296],{"class":242},[206,621,623,626],{"class":208,"line":622},18,[206,624,625],{"class":238},"    return",[206,627,628],{"class":242}," maintype, subtype\n",[10,630,631,632,635,636,639],{},"Getting the type right matters more than it sounds. Sent as ",[14,633,634],{},"application\u002Foctet-stream",", an ",[14,637,638],{},".xlsx"," arrives as a generic binary that some clients refuse to open and some gateways quarantine.",[10,641,642],{},"The attachment loop is then short:",[197,644,646],{"className":229,"code":645,"language":231,"meta":202,"style":202},"from email.message import EmailMessage\nfrom email.utils import formataddr\nfrom pathlib import Path\n\ndef attach_all(msg, paths):\n    \"\"\"Attach every file, each with its own MIME type. Returns what was added.\"\"\"\n    attached = []\n    for path in paths:\n        path = Path(path)\n        if not path.is_file():\n            raise FileNotFoundError(f\"cannot attach missing file: {path}\")\n        if path.stat().st_size == 0:\n            raise ValueError(f\"refusing to attach an empty file: {path}\")\n\n        maintype, subtype = mime_for(path)\n        msg.add_attachment(\n            path.read_bytes(),\n            maintype=maintype, subtype=subtype, filename=path.name,\n        )\n        attached.append(path.name)\n    return attached\n",[14,647,648,660,672,682,686,696,701,711,724,734,745,773,789,813,817,827,832,837,863,869,875],{"__ignoreMap":202},[206,649,650,652,655,657],{"class":208,"line":209},[206,651,239],{"class":238},[206,653,654],{"class":242}," email.message ",[206,656,246],{"class":238},[206,658,659],{"class":242}," EmailMessage\n",[206,661,662,664,667,669],{"class":208,"line":252},[206,663,239],{"class":238},[206,665,666],{"class":242}," email.utils ",[206,668,246],{"class":238},[206,670,671],{"class":242}," formataddr\n",[206,673,674,676,678,680],{"class":208,"line":266},[206,675,239],{"class":238},[206,677,243],{"class":242},[206,679,246],{"class":238},[206,681,249],{"class":242},[206,683,684],{"class":208,"line":273},[206,685,270],{"emptyLinePlaceholder":269},[206,687,688,690,693],{"class":208,"line":299},[206,689,532],{"class":238},[206,691,692],{"class":535}," attach_all",[206,694,695],{"class":242},"(msg, paths):\n",[206,697,698],{"class":208,"line":331},[206,699,700],{"class":216},"    \"\"\"Attach every file, each with its own MIME type. Returns what was added.\"\"\"\n",[206,702,703,706,708],{"class":208,"line":370},[206,704,705],{"class":242},"    attached ",[206,707,289],{"class":238},[206,709,710],{"class":242}," []\n",[206,712,713,716,719,721],{"class":208,"line":402},[206,714,715],{"class":238},"    for",[206,717,718],{"class":242}," path ",[206,720,308],{"class":238},[206,722,723],{"class":242}," paths:\n",[206,725,726,729,731],{"class":208,"line":505},[206,727,728],{"class":242},"        path ",[206,730,289],{"class":238},[206,732,733],{"class":242}," Path(path)\n",[206,735,736,739,742],{"class":208,"line":518},[206,737,738],{"class":238},"        if",[206,740,741],{"class":238}," not",[206,743,744],{"class":242}," path.is_file():\n",[206,746,747,750,753,756,759,762,764,766,768,771],{"class":208,"line":524},[206,748,749],{"class":238},"            raise",[206,751,752],{"class":292}," FileNotFoundError",[206,754,755],{"class":242},"(",[206,757,758],{"class":238},"f",[206,760,761],{"class":216},"\"cannot attach missing file: ",[206,763,380],{"class":379},[206,765,107],{"class":242},[206,767,386],{"class":379},[206,769,770],{"class":216},"\"",[206,772,296],{"class":242},[206,774,775,777,780,783,786],{"class":208,"line":529},[206,776,738],{"class":238},[206,778,779],{"class":242}," path.stat().st_size ",[206,781,782],{"class":238},"==",[206,784,785],{"class":292}," 0",[206,787,788],{"class":242},":\n",[206,790,791,793,796,798,800,803,805,807,809,811],{"class":208,"line":542},[206,792,749],{"class":238},[206,794,795],{"class":292}," ValueError",[206,797,755],{"class":242},[206,799,758],{"class":238},[206,801,802],{"class":216},"\"refusing to attach an empty file: ",[206,804,380],{"class":379},[206,806,107],{"class":242},[206,808,386],{"class":379},[206,810,770],{"class":216},[206,812,296],{"class":242},[206,814,815],{"class":208,"line":548},[206,816,270],{"emptyLinePlaceholder":269},[206,818,819,822,824],{"class":208,"line":559},[206,820,821],{"class":242},"        maintype, subtype ",[206,823,289],{"class":238},[206,825,826],{"class":242}," mime_for(path)\n",[206,828,829],{"class":208,"line":591},[206,830,831],{"class":242},"        msg.add_attachment(\n",[206,833,834],{"class":208,"line":606},[206,835,836],{"class":242},"            path.read_bytes(),\n",[206,838,839,842,844,847,850,852,855,858,860],{"class":208,"line":622},[206,840,841],{"class":285},"            maintype",[206,843,289],{"class":238},[206,845,846],{"class":242},"maintype, ",[206,848,849],{"class":285},"subtype",[206,851,289],{"class":238},[206,853,854],{"class":242},"subtype, ",[206,856,857],{"class":285},"filename",[206,859,289],{"class":238},[206,861,862],{"class":242},"path.name,\n",[206,864,866],{"class":208,"line":865},19,[206,867,868],{"class":242},"        )\n",[206,870,872],{"class":208,"line":871},20,[206,873,874],{"class":242},"        attached.append(path.name)\n",[206,876,878,880],{"class":208,"line":877},21,[206,879,625],{"class":238},[206,881,882],{"class":242}," attached\n",[10,884,885],{},"Refusing an empty file is worth the two lines. A zero-byte attachment is almost always an upstream failure, and sending it turns a job failure into a support conversation.",[192,887,889],{"id":888},"step-2-budget-the-size-before-building","Step 2 — Budget the size before building",[10,891,892,893,897],{},"Base64 inflates every attachment by roughly a third, and the limit that matters is the ",[894,895,896],"strong",{},"encoded"," size — plus the receiving server's limit, which you do not control and which may be lower than your own:",[197,899,901],{"className":229,"code":900,"language":231,"meta":202,"style":202},"from pathlib import Path\n\nBASE64_OVERHEAD = 4 \u002F 3\n\ndef encoded_size_mb(paths):\n    \"\"\"Approximate on-the-wire size of a set of attachments.\"\"\"\n    raw = sum(Path(p).stat().st_size for p in paths)\n    return raw * BASE64_OVERHEAD \u002F (1024 * 1024)\n\ndef check_budget(paths, limit_mb=20):\n    size = encoded_size_mb(paths)\n    if size > limit_mb:\n        biggest = max(paths, key=lambda p: Path(p).stat().st_size)\n        raise ValueError(\n            f\"attachments encode to about {size:.1f} MB, over the {limit_mb} MB \"\n            f\"budget. Largest is {Path(biggest).name}. Zip them, or publish \"\n            f\"and send a link.\"\n        )\n    return size\n",[14,902,903,913,917,933,937,947,952,975,1003,1007,1023,1033,1047,1069,1079,1110,1127,1134,1138],{"__ignoreMap":202},[206,904,905,907,909,911],{"class":208,"line":209},[206,906,239],{"class":238},[206,908,243],{"class":242},[206,910,246],{"class":238},[206,912,249],{"class":242},[206,914,915],{"class":208,"line":252},[206,916,270],{"emptyLinePlaceholder":269},[206,918,919,922,924,927,930],{"class":208,"line":266},[206,920,921],{"class":292},"BASE64_OVERHEAD",[206,923,449],{"class":238},[206,925,926],{"class":292}," 4",[206,928,929],{"class":238}," \u002F",[206,931,932],{"class":292}," 3\n",[206,934,935],{"class":208,"line":273},[206,936,270],{"emptyLinePlaceholder":269},[206,938,939,941,944],{"class":208,"line":299},[206,940,532],{"class":238},[206,942,943],{"class":535}," encoded_size_mb",[206,945,946],{"class":242},"(paths):\n",[206,948,949],{"class":208,"line":331},[206,950,951],{"class":216},"    \"\"\"Approximate on-the-wire size of a set of attachments.\"\"\"\n",[206,953,954,957,959,962,965,967,970,972],{"class":208,"line":370},[206,955,956],{"class":242},"    raw ",[206,958,289],{"class":238},[206,960,961],{"class":292}," sum",[206,963,964],{"class":242},"(Path(p).stat().st_size ",[206,966,302],{"class":238},[206,968,969],{"class":242}," p ",[206,971,308],{"class":238},[206,973,974],{"class":242}," paths)\n",[206,976,977,979,982,985,988,990,992,995,998,1001],{"class":208,"line":402},[206,978,625],{"class":238},[206,980,981],{"class":242}," raw ",[206,983,984],{"class":238},"*",[206,986,987],{"class":292}," BASE64_OVERHEAD",[206,989,929],{"class":238},[206,991,311],{"class":242},[206,993,994],{"class":292},"1024",[206,996,997],{"class":238}," *",[206,999,1000],{"class":292}," 1024",[206,1002,296],{"class":242},[206,1004,1005],{"class":208,"line":505},[206,1006,270],{"emptyLinePlaceholder":269},[206,1008,1009,1011,1014,1017,1019,1021],{"class":208,"line":518},[206,1010,532],{"class":238},[206,1012,1013],{"class":535}," check_budget",[206,1015,1016],{"class":242},"(paths, limit_mb",[206,1018,289],{"class":238},[206,1020,61],{"class":292},[206,1022,328],{"class":242},[206,1024,1025,1028,1030],{"class":208,"line":524},[206,1026,1027],{"class":242},"    size ",[206,1029,289],{"class":238},[206,1031,1032],{"class":242}," encoded_size_mb(paths)\n",[206,1034,1035,1038,1041,1044],{"class":208,"line":529},[206,1036,1037],{"class":238},"    if",[206,1039,1040],{"class":242}," size ",[206,1042,1043],{"class":238},">",[206,1045,1046],{"class":242}," limit_mb:\n",[206,1048,1049,1052,1054,1057,1060,1063,1066],{"class":208,"line":542},[206,1050,1051],{"class":242},"        biggest ",[206,1053,289],{"class":238},[206,1055,1056],{"class":292}," max",[206,1058,1059],{"class":242},"(paths, ",[206,1061,1062],{"class":285},"key",[206,1064,1065],{"class":238},"=lambda",[206,1067,1068],{"class":242}," p: Path(p).stat().st_size)\n",[206,1070,1071,1074,1076],{"class":208,"line":548},[206,1072,1073],{"class":238},"        raise",[206,1075,795],{"class":292},[206,1077,1078],{"class":242},"(\n",[206,1080,1081,1084,1087,1089,1092,1095,1097,1100,1102,1105,1107],{"class":208,"line":559},[206,1082,1083],{"class":238},"            f",[206,1085,1086],{"class":216},"\"attachments encode to about ",[206,1088,380],{"class":379},[206,1090,1091],{"class":242},"size",[206,1093,1094],{"class":238},":.1f",[206,1096,386],{"class":379},[206,1098,1099],{"class":216}," MB, over the ",[206,1101,380],{"class":379},[206,1103,1104],{"class":242},"limit_mb",[206,1106,386],{"class":379},[206,1108,1109],{"class":216}," MB \"\n",[206,1111,1112,1114,1117,1119,1122,1124],{"class":208,"line":591},[206,1113,1083],{"class":238},[206,1115,1116],{"class":216},"\"budget. Largest is ",[206,1118,380],{"class":379},[206,1120,1121],{"class":242},"Path(biggest).name",[206,1123,386],{"class":379},[206,1125,1126],{"class":216},". Zip them, or publish \"\n",[206,1128,1129,1131],{"class":208,"line":606},[206,1130,1083],{"class":238},[206,1132,1133],{"class":216},"\"and send a link.\"\n",[206,1135,1136],{"class":208,"line":622},[206,1137,868],{"class":242},[206,1139,1140,1142],{"class":208,"line":865},[206,1141,625],{"class":238},[206,1143,1144],{"class":242}," size\n",[10,1146,1147],{},"Setting the budget below the provider's actual limit leaves headroom for the body, the headers and any gateway that rewrites the message on the way through.",[192,1149,1151],{"id":1150},"step-3-zip-when-there-are-many","Step 3 — Zip when there are many",[10,1153,1154],{},"Beyond a handful of files, a single archive is both smaller and easier for the recipient. Build it in memory so no temporary files are left behind:",[197,1156,1158],{"className":229,"code":1157,"language":231,"meta":202,"style":202},"import io\nimport zipfile\nfrom pathlib import Path\n\ndef zip_bytes(paths, arcname_prefix=\"\"):\n    \"\"\"Zip a set of files into memory and return (bytes, per-file sizes).\"\"\"\n    buffer = io.BytesIO()\n    sizes = {}\n    with zipfile.ZipFile(buffer, \"w\", zipfile.ZIP_DEFLATED,\n                         compresslevel=6) as archive:\n        for path in paths:\n            path = Path(path)\n            archive.write(path, arcname=f\"{arcname_prefix}{path.name}\")\n            sizes[path.name] = path.stat().st_size\n    return buffer.getvalue(), sizes\n",[14,1159,1160,1167,1174,1184,1188,1205,1210,1220,1230,1249,1266,1277,1286,1317,1327],{"__ignoreMap":202},[206,1161,1162,1164],{"class":208,"line":209},[206,1163,246],{"class":238},[206,1165,1166],{"class":242}," io\n",[206,1168,1169,1171],{"class":208,"line":252},[206,1170,246],{"class":238},[206,1172,1173],{"class":242}," zipfile\n",[206,1175,1176,1178,1180,1182],{"class":208,"line":266},[206,1177,239],{"class":238},[206,1179,243],{"class":242},[206,1181,246],{"class":238},[206,1183,249],{"class":242},[206,1185,1186],{"class":208,"line":273},[206,1187,270],{"emptyLinePlaceholder":269},[206,1189,1190,1192,1195,1198,1200,1203],{"class":208,"line":299},[206,1191,532],{"class":238},[206,1193,1194],{"class":535}," zip_bytes",[206,1196,1197],{"class":242},"(paths, arcname_prefix",[206,1199,289],{"class":238},[206,1201,1202],{"class":216},"\"\"",[206,1204,328],{"class":242},[206,1206,1207],{"class":208,"line":331},[206,1208,1209],{"class":216},"    \"\"\"Zip a set of files into memory and return (bytes, per-file sizes).\"\"\"\n",[206,1211,1212,1215,1217],{"class":208,"line":370},[206,1213,1214],{"class":242},"    buffer ",[206,1216,289],{"class":238},[206,1218,1219],{"class":242}," io.BytesIO()\n",[206,1221,1222,1225,1227],{"class":208,"line":402},[206,1223,1224],{"class":242},"    sizes ",[206,1226,289],{"class":238},[206,1228,1229],{"class":242}," {}\n",[206,1231,1232,1235,1238,1241,1244,1247],{"class":208,"line":505},[206,1233,1234],{"class":238},"    with",[206,1236,1237],{"class":242}," zipfile.ZipFile(buffer, ",[206,1239,1240],{"class":216},"\"w\"",[206,1242,1243],{"class":242},", zipfile.",[206,1245,1246],{"class":292},"ZIP_DEFLATED",[206,1248,466],{"class":242},[206,1250,1251,1254,1256,1258,1261,1263],{"class":208,"line":518},[206,1252,1253],{"class":285},"                         compresslevel",[206,1255,289],{"class":238},[206,1257,65],{"class":292},[206,1259,1260],{"class":242},") ",[206,1262,260],{"class":238},[206,1264,1265],{"class":242}," archive:\n",[206,1267,1268,1271,1273,1275],{"class":208,"line":524},[206,1269,1270],{"class":238},"        for",[206,1272,718],{"class":242},[206,1274,308],{"class":238},[206,1276,723],{"class":242},[206,1278,1279,1282,1284],{"class":208,"line":529},[206,1280,1281],{"class":242},"            path ",[206,1283,289],{"class":238},[206,1285,733],{"class":242},[206,1287,1288,1291,1294,1296,1298,1300,1302,1305,1308,1311,1313,1315],{"class":208,"line":542},[206,1289,1290],{"class":242},"            archive.write(path, ",[206,1292,1293],{"class":285},"arcname",[206,1295,289],{"class":238},[206,1297,758],{"class":238},[206,1299,770],{"class":216},[206,1301,380],{"class":379},[206,1303,1304],{"class":242},"arcname_prefix",[206,1306,1307],{"class":379},"}{",[206,1309,1310],{"class":242},"path.name",[206,1312,386],{"class":379},[206,1314,770],{"class":216},[206,1316,296],{"class":242},[206,1318,1319,1322,1324],{"class":208,"line":548},[206,1320,1321],{"class":242},"            sizes[path.name] ",[206,1323,289],{"class":238},[206,1325,1326],{"class":242}," path.stat().st_size\n",[206,1328,1329,1331],{"class":208,"line":559},[206,1330,625],{"class":238},[206,1332,1333],{"class":242}," buffer.getvalue(), sizes\n",[10,1335,1336,1337,1339,1340,1343],{},"Be realistic about the gain. An ",[14,1338,638],{}," is already a zip of compressed XML, so a single workbook typically shrinks by only a few per cent. What zipping actually buys you is ",[894,1341,1342],{},"one attachment instead of twenty",", plus real compression on any CSV or text files in the set — those often compress by eighty per cent or more.",[10,1345,1346,1347,1350],{},"Two cautions. Many corporate gateways block ",[14,1348,1349],{},".zip"," attachments outright, so confirm the recipients can receive one before relying on it. And never password-protect the archive and send the password in the same message; that provides no protection and often triggers the gateway rules that block encrypted archives.",[192,1352,1354],{"id":1353},"step-4-decide-between-attaching-zipping-and-linking","Step 4 — Decide between attaching, zipping and linking",[10,1356,1357],{},"The choice follows from the count and the size, and it is worth encoding as a rule rather than deciding case by case.",[25,1359,35,1366,35,1369,35,1372,35,1376,35,1382,35,1388,35,1393,35,1397,35,1402,35,1406,35,1410,35,1413,35,1418,35,1421,35,1424,35,1427,35,1430,35,1433,35,1436,35,1441,35,1444,35,1447,35,1450,35,1453],{"viewBox":1360,"role":28,"ariaLabel":1361,"ariaLabelledBy":1362,"xmlns":33,"style":1365},"0 0 800 244","A decision rule for attachments: few small files attach individually, many small files zip into one archive, and anything large is published and linked.",[1363,1364],"dec-t","dec-d","width:100%;max-width:800px;height:auto;display:block;margin:1.5rem auto;font-family:Inter,ui-sans-serif,system-ui,sans-serif",[37,1367,1368],{"id":1363},"Attach, zip, or publish and link",[41,1370,1371],{"id":1364},"Three bands by file count and total size. Up to about five files totalling under ten megabytes, attach them individually so recipients can open one without unpacking anything. More than five small files are better as a single zip archive, provided the recipients' gateway accepts zips. Anything totalling more than about ten megabytes should be published to storage and linked, because the message will otherwise be near or over the gateway limit.",[45,1373],{"x":585,"y":585,"width":1374,"height":1375,"fill":51},"800","244",[45,1377],{"x":1378,"y":1379,"width":1380,"height":1381,"rx":1378,"fill":94,"stroke":95,"style":68},"14","26","248","192",[53,1383,1387],{"x":1384,"y":1385,"style":1386},"138","56","font-size:12.5px;font-weight:700;fill:var(--teal-ink,#0b6157);text-anchor:middle","attach individually",[53,1389,1392],{"x":1384,"y":1390,"style":1391},"88","font-size:11.5px;fill:var(--text,#172033);text-anchor:middle","up to ~5 files",[53,1394,1396],{"x":1384,"y":1395,"style":1391},"112","under ~10 MB total",[53,1398,1401],{"x":1384,"y":1399,"style":1400},"148","font-size:11px;fill:var(--muted,#5b6780);text-anchor:middle","recipients open one file",[53,1403,1405],{"x":1384,"y":1404,"style":1400},"168","without unpacking anything",[53,1407,1409],{"x":1384,"y":1408,"style":170},"200","the friendliest option",[45,1411],{"x":1412,"y":1379,"width":1380,"height":1381,"rx":1378,"fill":84,"stroke":85,"style":68},"276",[53,1414,1417],{"x":1415,"y":1385,"style":1416},"400","font-size:12.5px;font-weight:700;fill:var(--gold-ink,#7a4e06);text-anchor:middle","zip into one archive",[53,1419,1420],{"x":1415,"y":1390,"style":1391},"more than ~5 files",[53,1422,1423],{"x":1415,"y":1395,"style":1391},"still under the limit",[53,1425,1426],{"x":1415,"y":1399,"style":1400},"one attachment, not twenty",[53,1428,1429],{"x":1415,"y":1404,"style":1400},"big gain on CSV, small on xlsx",[53,1431,1432],{"x":1415,"y":1408,"style":185},"check the gateway allows zips",[45,1434],{"x":1435,"y":1379,"width":1380,"height":1381,"rx":1378,"fill":66,"stroke":67,"style":68},"538",[53,1437,1440],{"x":1438,"y":1385,"style":1439},"662","font-size:12.5px;font-weight:700;fill:var(--brand-strong,#4338ca);text-anchor:middle","publish and link",[53,1442,1443],{"x":1438,"y":1390,"style":1391},"over ~10 MB total",[53,1445,1446],{"x":1438,"y":1395,"style":1391},"or many recipients",[53,1448,1449],{"x":1438,"y":1399,"style":1400},"one stored copy, not one",[53,1451,1452],{"x":1438,"y":1404,"style":1400},"per mailbox",[53,1454,1456],{"x":1438,"y":1408,"style":1455},"font-size:11px;font-weight:700;fill:var(--brand-strong,#4338ca);text-anchor:middle","scales without limit",[197,1458,1460],{"className":229,"code":1459,"language":231,"meta":202,"style":202},"from email.message import EmailMessage\nfrom email.utils import formataddr\nfrom pathlib import Path\n\ndef build_multi_attachment(paths, sender, recipients, subject, body,\n                           zip_over=5, limit_mb=20, zip_name=\"reports.zip\"):\n    \"\"\"One message carrying several files, zipping when there are many.\"\"\"\n    paths = [Path(p) for p in paths]\n    size_mb = encoded_size_mb(paths)\n\n    msg = EmailMessage()\n    msg[\"From\"] = formataddr((\"Reporting\", sender))\n    msg[\"To\"] = \", \".join(recipients)\n    msg[\"Subject\"] = subject\n\n    if size_mb > limit_mb:\n        raise ValueError(\n            f\"{size_mb:.1f} MB encoded — publish these and send a link instead\"\n        )\n\n    if len(paths) > zip_over:\n        payload, sizes = zip_bytes(paths)\n        listing = \"\\n\".join(f\"  {name}  ({size:,} bytes)\"\n                            for name, size in sizes.items())\n        msg.set_content(f\"{body}\\n\\nAttached as {zip_name}:\\n{listing}\\n\")\n        msg.add_attachment(payload, maintype=\"application\", subtype=\"zip\",\n                           filename=zip_name)\n        return msg, [zip_name]\n\n    listing = \"\\n\".join(f\"  {p.name}\" for p in paths)\n    msg.set_content(f\"{body}\\n\\nAttached:\\n{listing}\\n\")\n    return msg, attach_all(msg, paths)\n",[14,1461,1462,1472,1482,1492,1496,1506,1533,1538,1557,1566,1570,1580,1602,1619,1633,1637,1648,1656,1674,1678,1682,1697,1708,1754,1768,1812,1837,1848,1857,1862,1900,1929],{"__ignoreMap":202},[206,1463,1464,1466,1468,1470],{"class":208,"line":209},[206,1465,239],{"class":238},[206,1467,654],{"class":242},[206,1469,246],{"class":238},[206,1471,659],{"class":242},[206,1473,1474,1476,1478,1480],{"class":208,"line":252},[206,1475,239],{"class":238},[206,1477,666],{"class":242},[206,1479,246],{"class":238},[206,1481,671],{"class":242},[206,1483,1484,1486,1488,1490],{"class":208,"line":266},[206,1485,239],{"class":238},[206,1487,243],{"class":242},[206,1489,246],{"class":238},[206,1491,249],{"class":242},[206,1493,1494],{"class":208,"line":273},[206,1495,270],{"emptyLinePlaceholder":269},[206,1497,1498,1500,1503],{"class":208,"line":299},[206,1499,532],{"class":238},[206,1501,1502],{"class":535}," build_multi_attachment",[206,1504,1505],{"class":242},"(paths, sender, recipients, subject, body,\n",[206,1507,1508,1511,1513,1516,1519,1521,1523,1526,1528,1531],{"class":208,"line":331},[206,1509,1510],{"class":242},"                           zip_over",[206,1512,289],{"class":238},[206,1514,1515],{"class":292},"5",[206,1517,1518],{"class":242},", limit_mb",[206,1520,289],{"class":238},[206,1522,61],{"class":292},[206,1524,1525],{"class":242},", zip_name",[206,1527,289],{"class":238},[206,1529,1530],{"class":216},"\"reports.zip\"",[206,1532,328],{"class":242},[206,1534,1535],{"class":208,"line":370},[206,1536,1537],{"class":216},"    \"\"\"One message carrying several files, zipping when there are many.\"\"\"\n",[206,1539,1540,1543,1545,1548,1550,1552,1554],{"class":208,"line":402},[206,1541,1542],{"class":242},"    paths ",[206,1544,289],{"class":238},[206,1546,1547],{"class":242}," [Path(p) ",[206,1549,302],{"class":238},[206,1551,969],{"class":242},[206,1553,308],{"class":238},[206,1555,1556],{"class":242}," paths]\n",[206,1558,1559,1562,1564],{"class":208,"line":505},[206,1560,1561],{"class":242},"    size_mb ",[206,1563,289],{"class":238},[206,1565,1032],{"class":242},[206,1567,1568],{"class":208,"line":518},[206,1569,270],{"emptyLinePlaceholder":269},[206,1571,1572,1575,1577],{"class":208,"line":524},[206,1573,1574],{"class":242},"    msg ",[206,1576,289],{"class":238},[206,1578,1579],{"class":242}," EmailMessage()\n",[206,1581,1582,1585,1588,1591,1593,1596,1599],{"class":208,"line":529},[206,1583,1584],{"class":242},"    msg[",[206,1586,1587],{"class":216},"\"From\"",[206,1589,1590],{"class":242},"] ",[206,1592,289],{"class":238},[206,1594,1595],{"class":242}," formataddr((",[206,1597,1598],{"class":216},"\"Reporting\"",[206,1600,1601],{"class":242},", sender))\n",[206,1603,1604,1606,1609,1611,1613,1616],{"class":208,"line":542},[206,1605,1584],{"class":242},[206,1607,1608],{"class":216},"\"To\"",[206,1610,1590],{"class":242},[206,1612,289],{"class":238},[206,1614,1615],{"class":216}," \", \"",[206,1617,1618],{"class":242},".join(recipients)\n",[206,1620,1621,1623,1626,1628,1630],{"class":208,"line":548},[206,1622,1584],{"class":242},[206,1624,1625],{"class":216},"\"Subject\"",[206,1627,1590],{"class":242},[206,1629,289],{"class":238},[206,1631,1632],{"class":242}," subject\n",[206,1634,1635],{"class":208,"line":559},[206,1636,270],{"emptyLinePlaceholder":269},[206,1638,1639,1641,1644,1646],{"class":208,"line":591},[206,1640,1037],{"class":238},[206,1642,1643],{"class":242}," size_mb ",[206,1645,1043],{"class":238},[206,1647,1046],{"class":242},[206,1649,1650,1652,1654],{"class":208,"line":606},[206,1651,1073],{"class":238},[206,1653,795],{"class":292},[206,1655,1078],{"class":242},[206,1657,1658,1660,1662,1664,1667,1669,1671],{"class":208,"line":622},[206,1659,1083],{"class":238},[206,1661,770],{"class":216},[206,1663,380],{"class":379},[206,1665,1666],{"class":242},"size_mb",[206,1668,1094],{"class":238},[206,1670,386],{"class":379},[206,1672,1673],{"class":216}," MB encoded — publish these and send a link instead\"\n",[206,1675,1676],{"class":208,"line":865},[206,1677,868],{"class":242},[206,1679,1680],{"class":208,"line":871},[206,1681,270],{"emptyLinePlaceholder":269},[206,1683,1684,1686,1689,1692,1694],{"class":208,"line":877},[206,1685,1037],{"class":238},[206,1687,1688],{"class":292}," len",[206,1690,1691],{"class":242},"(paths) ",[206,1693,1043],{"class":238},[206,1695,1696],{"class":242}," zip_over:\n",[206,1698,1700,1703,1705],{"class":208,"line":1699},22,[206,1701,1702],{"class":242},"        payload, sizes ",[206,1704,289],{"class":238},[206,1706,1707],{"class":242}," zip_bytes(paths)\n",[206,1709,1711,1714,1716,1719,1722,1724,1727,1729,1732,1734,1737,1739,1742,1744,1746,1749,1751],{"class":208,"line":1710},23,[206,1712,1713],{"class":242},"        listing ",[206,1715,289],{"class":238},[206,1717,1718],{"class":216}," \"",[206,1720,1721],{"class":379},"\\n",[206,1723,770],{"class":216},[206,1725,1726],{"class":242},".join(",[206,1728,758],{"class":238},[206,1730,1731],{"class":216},"\"  ",[206,1733,380],{"class":379},[206,1735,1736],{"class":242},"name",[206,1738,386],{"class":379},[206,1740,1741],{"class":216},"  (",[206,1743,380],{"class":379},[206,1745,1091],{"class":242},[206,1747,1748],{"class":238},":,",[206,1750,386],{"class":379},[206,1752,1753],{"class":216}," bytes)\"\n",[206,1755,1757,1760,1763,1765],{"class":208,"line":1756},24,[206,1758,1759],{"class":238},"                            for",[206,1761,1762],{"class":242}," name, size ",[206,1764,308],{"class":238},[206,1766,1767],{"class":242}," sizes.items())\n",[206,1769,1771,1774,1776,1778,1780,1783,1786,1789,1791,1794,1796,1799,1802,1805,1808,1810],{"class":208,"line":1770},25,[206,1772,1773],{"class":242},"        msg.set_content(",[206,1775,758],{"class":238},[206,1777,770],{"class":216},[206,1779,380],{"class":379},[206,1781,1782],{"class":242},"body",[206,1784,1785],{"class":379},"}\\n\\n",[206,1787,1788],{"class":216},"Attached as ",[206,1790,380],{"class":379},[206,1792,1793],{"class":242},"zip_name",[206,1795,386],{"class":379},[206,1797,1798],{"class":216},":",[206,1800,1801],{"class":379},"\\n{",[206,1803,1804],{"class":242},"listing",[206,1806,1807],{"class":379},"}\\n",[206,1809,770],{"class":216},[206,1811,296],{"class":242},[206,1813,1815,1818,1821,1823,1826,1828,1830,1832,1835],{"class":208,"line":1814},26,[206,1816,1817],{"class":242},"        msg.add_attachment(payload, ",[206,1819,1820],{"class":285},"maintype",[206,1822,289],{"class":238},[206,1824,1825],{"class":216},"\"application\"",[206,1827,317],{"class":242},[206,1829,849],{"class":285},[206,1831,289],{"class":238},[206,1833,1834],{"class":216},"\"zip\"",[206,1836,466],{"class":242},[206,1838,1840,1843,1845],{"class":208,"line":1839},27,[206,1841,1842],{"class":285},"                           filename",[206,1844,289],{"class":238},[206,1846,1847],{"class":242},"zip_name)\n",[206,1849,1851,1854],{"class":208,"line":1850},28,[206,1852,1853],{"class":238},"        return",[206,1855,1856],{"class":242}," msg, [zip_name]\n",[206,1858,1860],{"class":208,"line":1859},29,[206,1861,270],{"emptyLinePlaceholder":269},[206,1863,1865,1868,1870,1872,1874,1876,1878,1880,1882,1884,1887,1889,1891,1894,1896,1898],{"class":208,"line":1864},30,[206,1866,1867],{"class":242},"    listing ",[206,1869,289],{"class":238},[206,1871,1718],{"class":216},[206,1873,1721],{"class":379},[206,1875,770],{"class":216},[206,1877,1726],{"class":242},[206,1879,758],{"class":238},[206,1881,1731],{"class":216},[206,1883,380],{"class":379},[206,1885,1886],{"class":242},"p.name",[206,1888,386],{"class":379},[206,1890,770],{"class":216},[206,1892,1893],{"class":238}," for",[206,1895,969],{"class":242},[206,1897,308],{"class":238},[206,1899,974],{"class":242},[206,1901,1903,1906,1908,1910,1912,1914,1916,1919,1921,1923,1925,1927],{"class":208,"line":1902},31,[206,1904,1905],{"class":242},"    msg.set_content(",[206,1907,758],{"class":238},[206,1909,770],{"class":216},[206,1911,380],{"class":379},[206,1913,1782],{"class":242},[206,1915,1785],{"class":379},[206,1917,1918],{"class":216},"Attached:",[206,1920,1801],{"class":379},[206,1922,1804],{"class":242},[206,1924,1807],{"class":379},[206,1926,770],{"class":216},[206,1928,296],{"class":242},[206,1930,1932,1934],{"class":208,"line":1931},32,[206,1933,625],{"class":238},[206,1935,1936],{"class":242}," msg, attach_all(msg, paths)\n",[10,1938,1939],{},"Listing the filenames in the body is a small courtesy that pays off: a recipient whose client hides attachments, or whose gateway stripped one, can tell what was meant to arrive.",[192,1941,1943],{"id":1942},"step-5-verify-before-sending","Step 5 — Verify before sending",[25,1945,35,1952,35,1955,35,1958,35,1964,35,1969,35,1975,35,1980,35,1983,35,1987,35,1993,35,1996,35,2001,35,2003,35,2006,35,2009,35,2013,35,2017,35,2022,35,2027,35,2030],{"viewBox":1946,"role":28,"ariaLabel":1947,"ariaLabelledBy":1948,"xmlns":33,"style":1951},"-2 36 802 182","Verification walk: the built message's parts are enumerated and compared against the intended filenames, so a path that did not resolve is caught before sending.",[1949,1950],"verifym-t","verifym-d","width:100%;max-width:802px;height:auto;display:block;margin:1.5rem auto;font-family:Inter,ui-sans-serif,system-ui,sans-serif",[37,1953,1954],{"id":1949},"Compare what you meant to attach with what is in the message",[41,1956,1957],{"id":1950},"The intended list of four filenames is compared against the filenames actually present in the built message, found by walking its parts and keeping those whose content disposition is attachment. A file missing from the second list means a path did not resolve or a step was skipped. Catching that before sending turns a silently incomplete email into a job failure with a clear message.",[45,1959],{"x":1960,"y":1961,"width":1962,"height":1963,"fill":51},"-2","36","802","182",[45,1965],{"x":1378,"y":1966,"width":1967,"height":163,"rx":1968,"fill":66,"stroke":67,"style":68},"52","232","13",[53,1970,1974],{"x":1971,"y":1972,"style":1973},"130","80","font-size:11.5px;font-weight:700;fill:var(--brand-strong,#4338ca);text-anchor:middle","intended",[53,1976,1979],{"x":1971,"y":1977,"style":1978},"106","font-size:10.5px;fill:var(--text,#172033);text-anchor:middle","north.xlsx · south.xlsx",[53,1981,1982],{"x":1971,"y":139,"style":1978},"west.xlsx · east.xlsx",[53,1984,1986],{"x":1971,"y":1985,"style":175},"152","4 files",[53,1988,1992],{"x":1989,"y":1990,"style":1991},"277","118","font-size:16px;font-weight:700;fill:var(--brand,#5b5cf0);text-anchor:middle","vs",[45,1994],{"x":1995,"y":1966,"width":1967,"height":163,"rx":1968,"fill":94,"stroke":95,"style":68},"308",[53,1997,2000],{"x":1998,"y":1972,"style":1999},"424","font-size:11.5px;font-weight:700;fill:var(--teal-ink,#0b6157);text-anchor:middle","in the message",[53,2002,1979],{"x":1998,"y":1977,"style":1978},[53,2004,2005],{"x":1998,"y":139,"style":1978},"west.xlsx",[53,2007,2008],{"x":1998,"y":1985,"style":175},"3 files",[208,2010],{"x1":2011,"y1":1395,"x2":2012,"y2":1395,"stroke":181,"style":68},"540","572",[120,2014],{"points":2015,"fill":2016},"580,112 568,106 568,118","#f43f8f",[45,2018],{"x":2019,"y":127,"width":2020,"height":2021,"rx":130,"fill":180,"stroke":181,"style":68},"588","196","72",[53,2023,2026],{"x":2024,"y":134,"style":2025},"686","font-size:11.5px;font-weight:700;fill:var(--accent-ink,#be185d);text-anchor:middle","east.xlsx missing",[53,2028,2029],{"x":2024,"y":139,"style":175},"fail before sending",[53,2031,2032],{"x":1415,"y":1408,"style":1400},"three lines of comparison, and a silently incomplete email becomes a clear job failure",[10,2034,2035],{},"Walk the built message and confirm every intended file is there. It is three lines and it catches the case where a path silently did not resolve:",[197,2037,2039],{"className":229,"code":2038,"language":231,"meta":202,"style":202},"def attached_filenames(msg):\n    \"\"\"Every filename actually present in the built message.\"\"\"\n    return [\n        part.get_filename()\n        for part in msg.walk()\n        if part.get_content_disposition() == \"attachment\"\n    ]\n\nmsg, intended = build_multi_attachment(\n    sorted(Path(\"out\").glob(\"*.xlsx\")),\n    sender=\"reporting@example.com\",\n    recipients=[\"team@example.com\"],\n    subject=\"Regional reports — August 2026\",\n    body=\"The August regional reports are attached.\",\n)\n\npresent = attached_filenames(msg)\nmissing = set(intended) - set(present)\nassert not missing, f\"not attached: {sorted(missing)}\"\nprint(f\"{len(present)} attachment(s):\", present)\n",[14,2040,2041,2051,2056,2063,2068,2080,2092,2097,2101,2111,2130,2142,2158,2170,2182,2186,2190,2200,2221,2249],{"__ignoreMap":202},[206,2042,2043,2045,2048],{"class":208,"line":209},[206,2044,532],{"class":238},[206,2046,2047],{"class":535}," attached_filenames",[206,2049,2050],{"class":242},"(msg):\n",[206,2052,2053],{"class":208,"line":252},[206,2054,2055],{"class":216},"    \"\"\"Every filename actually present in the built message.\"\"\"\n",[206,2057,2058,2060],{"class":208,"line":266},[206,2059,625],{"class":238},[206,2061,2062],{"class":242}," [\n",[206,2064,2065],{"class":208,"line":273},[206,2066,2067],{"class":242},"        part.get_filename()\n",[206,2069,2070,2072,2075,2077],{"class":208,"line":299},[206,2071,1270],{"class":238},[206,2073,2074],{"class":242}," part ",[206,2076,308],{"class":238},[206,2078,2079],{"class":242}," msg.walk()\n",[206,2081,2082,2084,2087,2089],{"class":208,"line":331},[206,2083,738],{"class":238},[206,2085,2086],{"class":242}," part.get_content_disposition() ",[206,2088,782],{"class":238},[206,2090,2091],{"class":216}," \"attachment\"\n",[206,2093,2094],{"class":208,"line":370},[206,2095,2096],{"class":242},"    ]\n",[206,2098,2099],{"class":208,"line":402},[206,2100,270],{"emptyLinePlaceholder":269},[206,2102,2103,2106,2108],{"class":208,"line":505},[206,2104,2105],{"class":242},"msg, intended ",[206,2107,289],{"class":238},[206,2109,2110],{"class":242}," build_multi_attachment(\n",[206,2112,2113,2116,2119,2121,2124,2127],{"class":208,"line":518},[206,2114,2115],{"class":292},"    sorted",[206,2117,2118],{"class":242},"(Path(",[206,2120,279],{"class":216},[206,2122,2123],{"class":242},").glob(",[206,2125,2126],{"class":216},"\"*.xlsx\"",[206,2128,2129],{"class":242},")),\n",[206,2131,2132,2135,2137,2140],{"class":208,"line":524},[206,2133,2134],{"class":285},"    sender",[206,2136,289],{"class":238},[206,2138,2139],{"class":216},"\"reporting@example.com\"",[206,2141,466],{"class":242},[206,2143,2144,2147,2149,2152,2155],{"class":208,"line":529},[206,2145,2146],{"class":285},"    recipients",[206,2148,289],{"class":238},[206,2150,2151],{"class":242},"[",[206,2153,2154],{"class":216},"\"team@example.com\"",[206,2156,2157],{"class":242},"],\n",[206,2159,2160,2163,2165,2168],{"class":208,"line":542},[206,2161,2162],{"class":285},"    subject",[206,2164,289],{"class":238},[206,2166,2167],{"class":216},"\"Regional reports — August 2026\"",[206,2169,466],{"class":242},[206,2171,2172,2175,2177,2180],{"class":208,"line":548},[206,2173,2174],{"class":285},"    body",[206,2176,289],{"class":238},[206,2178,2179],{"class":216},"\"The August regional reports are attached.\"",[206,2181,466],{"class":242},[206,2183,2184],{"class":208,"line":559},[206,2185,296],{"class":242},[206,2187,2188],{"class":208,"line":591},[206,2189,270],{"emptyLinePlaceholder":269},[206,2191,2192,2195,2197],{"class":208,"line":606},[206,2193,2194],{"class":242},"present ",[206,2196,289],{"class":238},[206,2198,2199],{"class":242}," attached_filenames(msg)\n",[206,2201,2202,2205,2207,2210,2213,2216,2218],{"class":208,"line":622},[206,2203,2204],{"class":242},"missing ",[206,2206,289],{"class":238},[206,2208,2209],{"class":292}," set",[206,2211,2212],{"class":242},"(intended) ",[206,2214,2215],{"class":238},"-",[206,2217,2209],{"class":292},[206,2219,2220],{"class":242},"(present)\n",[206,2222,2223,2226,2228,2231,2233,2236,2238,2241,2244,2246],{"class":208,"line":865},[206,2224,2225],{"class":238},"assert",[206,2227,741],{"class":238},[206,2229,2230],{"class":242}," missing, ",[206,2232,758],{"class":238},[206,2234,2235],{"class":216},"\"not attached: ",[206,2237,380],{"class":379},[206,2239,2240],{"class":292},"sorted",[206,2242,2243],{"class":242},"(missing)",[206,2245,386],{"class":379},[206,2247,2248],{"class":216},"\"\n",[206,2250,2251,2254,2256,2258,2260,2262,2265,2268,2270,2273],{"class":208,"line":871},[206,2252,2253],{"class":292},"print",[206,2255,755],{"class":242},[206,2257,758],{"class":238},[206,2259,770],{"class":216},[206,2261,380],{"class":379},[206,2263,2264],{"class":292},"len",[206,2266,2267],{"class":242},"(present)",[206,2269,386],{"class":379},[206,2271,2272],{"class":216}," attachment(s):\"",[206,2274,2275],{"class":242},", present)\n",[10,2277,2278,2279,1798],{},"Then send with a single connection, as covered in the ",[19,2280,2281],{"href":21},"parent topic",[197,2283,2285],{"className":229,"code":2284,"language":231,"meta":202,"style":202},"import os\nimport smtplib\n\nwith 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    smtp.send_message(msg)\n",[14,2286,2287,2294,2301,2305,2318,2351,2356,2373],{"__ignoreMap":202},[206,2288,2289,2291],{"class":208,"line":209},[206,2290,246],{"class":238},[206,2292,2293],{"class":242}," os\n",[206,2295,2296,2298],{"class":208,"line":252},[206,2297,246],{"class":238},[206,2299,2300],{"class":242}," smtplib\n",[206,2302,2303],{"class":208,"line":266},[206,2304,270],{"emptyLinePlaceholder":269},[206,2306,2307,2310,2313,2316],{"class":208,"line":273},[206,2308,2309],{"class":238},"with",[206,2311,2312],{"class":242}," smtplib.SMTP(os.environ[",[206,2314,2315],{"class":216},"\"SMTP_HOST\"",[206,2317,2157],{"class":242},[206,2319,2320,2323,2326,2329,2331,2334,2337,2340,2342,2344,2346,2348],{"class":208,"line":299},[206,2321,2322],{"class":292},"                  int",[206,2324,2325],{"class":242},"(os.environ.get(",[206,2327,2328],{"class":216},"\"SMTP_PORT\"",[206,2330,317],{"class":242},[206,2332,2333],{"class":292},"587",[206,2335,2336],{"class":242},")), ",[206,2338,2339],{"class":285},"timeout",[206,2341,289],{"class":238},[206,2343,64],{"class":292},[206,2345,1260],{"class":242},[206,2347,260],{"class":238},[206,2349,2350],{"class":242}," smtp:\n",[206,2352,2353],{"class":208,"line":331},[206,2354,2355],{"class":242},"    smtp.starttls()\n",[206,2357,2358,2361,2364,2367,2370],{"class":208,"line":370},[206,2359,2360],{"class":242},"    smtp.login(os.environ[",[206,2362,2363],{"class":216},"\"SMTP_USER\"",[206,2365,2366],{"class":242},"], os.environ[",[206,2368,2369],{"class":216},"\"SMTP_PASSWORD\"",[206,2371,2372],{"class":242},"])\n",[206,2374,2375],{"class":208,"line":402},[206,2376,2377],{"class":242},"    smtp.send_message(msg)\n",[192,2379,2381],{"id":2380},"common-pitfalls-and-fixes","Common pitfalls and fixes",[2383,2384,2385,2401],"table",{},[2386,2387,2388],"thead",{},[2389,2390,2391,2395,2398],"tr",{},[2392,2393,2394],"th",{},"Symptom",[2392,2396,2397],{},"Cause",[2392,2399,2400],{},"Fix",[2402,2403,2404,2419,2430,2441,2452,2463,2476,2487],"tbody",{},[2389,2405,2406,2410,2416],{},[2407,2408,2409],"td",{},"Attachment will not open",[2407,2411,2412,2413],{},"Sent as ",[14,2414,2415],{},"octet-stream",[2407,2417,2418],{},"Set the correct MIME subtype per file.",[2389,2420,2421,2424,2427],{},[2407,2422,2423],{},"Message bounces as too large",[2407,2425,2426],{},"Budgeted the raw size",[2407,2428,2429],{},"Multiply by 4\u002F3 for base64.",[2389,2431,2432,2435,2438],{},[2407,2433,2434],{},"A file silently missing",[2407,2436,2437],{},"Path did not resolve",[2407,2439,2440],{},"Raise on a missing path; verify the parts.",[2389,2442,2443,2446,2449],{},[2407,2444,2445],{},"Zero-byte attachment sent",[2407,2447,2448],{},"Upstream produced nothing",[2407,2450,2451],{},"Refuse empty files.",[2389,2453,2454,2457,2460],{},[2407,2455,2456],{},"Zip stripped by the gateway",[2407,2458,2459],{},"Corporate policy blocks archives",[2407,2461,2462],{},"Attach individually, or link.",[2389,2464,2465,2468,2473],{},[2407,2466,2467],{},"Zipping barely helped",[2407,2469,2470,2472],{},[14,2471,638],{}," is already compressed",[2407,2474,2475],{},"The gain is one attachment, not size.",[2389,2477,2478,2481,2484],{},[2407,2479,2480],{},"Duplicate filenames in the zip",[2407,2482,2483],{},"Same name from different folders",[2407,2485,2486],{},"Prefix the archive names.",[2389,2488,2489,2492,2495],{},[2407,2490,2491],{},"Recipient sees no attachments",[2407,2493,2494],{},"Client hides them",[2407,2496,2497],{},"List the filenames in the body.",[192,2499,2501],{"id":2500},"performance-and-scale-notes","Performance and scale notes",[10,2503,2504],{},"The whole message is assembled in memory before sending, so peak usage is roughly the sum of the attachments plus their base64 expansion — call it 2.3 times the raw bytes. For a handful of report-sized files that is nothing; for a hundred it is worth thinking about.",[10,2506,2507,2508,2511],{},"Three habits. ",[894,2509,2510],{},"Read each file once."," Where the same workbook goes to several recipients, read the bytes once and reuse them across messages rather than re-reading per message:",[197,2513,2515],{"className":229,"code":2514,"language":231,"meta":202,"style":202},"from pathlib import Path\n\npayloads = {p.name: p.read_bytes() for p in Path(\"out\").glob(\"*.xlsx\")}\n",[14,2516,2517,2527,2531],{"__ignoreMap":202},[206,2518,2519,2521,2523,2525],{"class":208,"line":209},[206,2520,239],{"class":238},[206,2522,243],{"class":242},[206,2524,246],{"class":238},[206,2526,249],{"class":242},[206,2528,2529],{"class":208,"line":252},[206,2530,270],{"emptyLinePlaceholder":269},[206,2532,2533,2536,2538,2541,2543,2545,2547,2550,2552,2554,2556],{"class":208,"line":266},[206,2534,2535],{"class":242},"payloads ",[206,2537,289],{"class":238},[206,2539,2540],{"class":242}," {p.name: p.read_bytes() ",[206,2542,302],{"class":238},[206,2544,969],{"class":242},[206,2546,308],{"class":238},[206,2548,2549],{"class":242}," Path(",[206,2551,279],{"class":216},[206,2553,2123],{"class":242},[206,2555,2126],{"class":216},[206,2557,2558],{"class":242},")}\n",[10,2560,2561,2564,2565,2567,2568,2570],{},[894,2562,2563],{},"Compress only what compresses."," Running ",[14,2566,1246],{}," over already-compressed ",[14,2569,638],{}," files costs CPU for a few per cent. Store them and deflate only the text files:",[197,2572,2574],{"className":229,"code":2573,"language":231,"meta":202,"style":202},"import io, zipfile\nfrom pathlib import Path\n\nALREADY_COMPRESSED = {\".xlsx\", \".xlsm\", \".zip\", \".png\", \".jpg\", \".pdf\"}\n\ndef zip_smart(paths):\n    \"\"\"Deflate text-like files; store the ones already compressed.\"\"\"\n    buffer = io.BytesIO()\n    with zipfile.ZipFile(buffer, \"w\") as archive:\n        for path in map(Path, paths):\n            method = (zipfile.ZIP_STORED\n                      if path.suffix.lower() in ALREADY_COMPRESSED\n                      else zipfile.ZIP_DEFLATED)\n            archive.write(path, arcname=path.name, compress_type=method)\n    return buffer.getvalue()\n",[14,2575,2576,2583,2593,2597,2637,2641,2650,2655,2663,2677,2691,2704,2717,2729,2748],{"__ignoreMap":202},[206,2577,2578,2580],{"class":208,"line":209},[206,2579,246],{"class":238},[206,2581,2582],{"class":242}," io, zipfile\n",[206,2584,2585,2587,2589,2591],{"class":208,"line":252},[206,2586,239],{"class":238},[206,2588,243],{"class":242},[206,2590,246],{"class":238},[206,2592,249],{"class":242},[206,2594,2595],{"class":208,"line":266},[206,2596,270],{"emptyLinePlaceholder":269},[206,2598,2599,2602,2604,2607,2610,2612,2615,2617,2620,2622,2625,2627,2630,2632,2635],{"class":208,"line":273},[206,2600,2601],{"class":292},"ALREADY_COMPRESSED",[206,2603,449],{"class":238},[206,2605,2606],{"class":242}," {",[206,2608,2609],{"class":216},"\".xlsx\"",[206,2611,317],{"class":242},[206,2613,2614],{"class":216},"\".xlsm\"",[206,2616,317],{"class":242},[206,2618,2619],{"class":216},"\".zip\"",[206,2621,317],{"class":242},[206,2623,2624],{"class":216},"\".png\"",[206,2626,317],{"class":242},[206,2628,2629],{"class":216},"\".jpg\"",[206,2631,317],{"class":242},[206,2633,2634],{"class":216},"\".pdf\"",[206,2636,521],{"class":242},[206,2638,2639],{"class":208,"line":299},[206,2640,270],{"emptyLinePlaceholder":269},[206,2642,2643,2645,2648],{"class":208,"line":331},[206,2644,532],{"class":238},[206,2646,2647],{"class":535}," zip_smart",[206,2649,946],{"class":242},[206,2651,2652],{"class":208,"line":370},[206,2653,2654],{"class":216},"    \"\"\"Deflate text-like files; store the ones already compressed.\"\"\"\n",[206,2656,2657,2659,2661],{"class":208,"line":402},[206,2658,1214],{"class":242},[206,2660,289],{"class":238},[206,2662,1219],{"class":242},[206,2664,2665,2667,2669,2671,2673,2675],{"class":208,"line":505},[206,2666,1234],{"class":238},[206,2668,1237],{"class":242},[206,2670,1240],{"class":216},[206,2672,1260],{"class":242},[206,2674,260],{"class":238},[206,2676,1265],{"class":242},[206,2678,2679,2681,2683,2685,2688],{"class":208,"line":518},[206,2680,1270],{"class":238},[206,2682,718],{"class":242},[206,2684,308],{"class":238},[206,2686,2687],{"class":292}," map",[206,2689,2690],{"class":242},"(Path, paths):\n",[206,2692,2693,2696,2698,2701],{"class":208,"line":524},[206,2694,2695],{"class":242},"            method ",[206,2697,289],{"class":238},[206,2699,2700],{"class":242}," (zipfile.",[206,2702,2703],{"class":292},"ZIP_STORED\n",[206,2705,2706,2709,2712,2714],{"class":208,"line":529},[206,2707,2708],{"class":238},"                      if",[206,2710,2711],{"class":242}," path.suffix.lower() ",[206,2713,308],{"class":238},[206,2715,2716],{"class":292}," ALREADY_COMPRESSED\n",[206,2718,2719,2722,2725,2727],{"class":208,"line":542},[206,2720,2721],{"class":238},"                      else",[206,2723,2724],{"class":242}," zipfile.",[206,2726,1246],{"class":292},[206,2728,296],{"class":242},[206,2730,2731,2733,2735,2737,2740,2743,2745],{"class":208,"line":548},[206,2732,1290],{"class":242},[206,2734,1293],{"class":285},[206,2736,289],{"class":238},[206,2738,2739],{"class":242},"path.name, ",[206,2741,2742],{"class":285},"compress_type",[206,2744,289],{"class":238},[206,2746,2747],{"class":242},"method)\n",[206,2749,2750,2752],{"class":208,"line":559},[206,2751,625],{"class":238},[206,2753,2754],{"class":242}," buffer.getvalue()\n",[10,2756,2757,2760,2761,2765,2766,2770],{},[894,2758,2759],{},"Prefer publishing above a threshold."," Attachments scale multiplicatively — twenty recipients times a 10 MB message is 200 MB moved and stored — where a published file plus a link is one copy however many people receive it. The publish-and-link pattern is in ",[19,2762,2764],{"href":2763},"\u002Fautomating-reporting-workflows\u002Fpublishing-excel-reports-to-cloud-storage\u002F","publishing Excel reports to cloud storage",", and pairing it with a ",[19,2767,2769],{"href":2768},"\u002Fautomating-reporting-workflows\u002Femailing-excel-reports-with-smtplib\u002Femail-an-excel-report-with-an-html-summary-body\u002F","summary body"," keeps the email useful on its own.",[192,2772,2774],{"id":2773},"conclusion","Conclusion",[10,2776,2777,2778,2780],{},"Attaching several workbooks is a loop, wrapped in the checks that keep it reliable: the correct MIME type per file so clients will open them, a refusal on missing or empty files, and a size budget computed against the base64-encoded total rather than what the files measure on disk. Above about five files, send one zip instead — remembering that the gain is a single attachment rather than much compression, since ",[14,2779,638],{}," is already a zip. Above about ten megabytes, publish and link. And always verify the built message's parts before sending, because a path that quietly did not exist looks exactly like a message that worked.",[192,2782,2784],{"id":2783},"frequently-asked-questions","Frequently asked questions",[10,2786,2787,2790],{},[894,2788,2789],{},"Is there a limit on how many attachments one message can have?","\nNot in the format itself, but every gateway limits the total encoded size, and many recipients find more than a handful unwieldy. Above about five files, zip them or publish and link instead.",[10,2792,2793,2796],{},[894,2794,2795],{},"How much does base64 encoding add?","\nRoughly a third. Four megabytes of files become about 5.3 megabytes on the wire, so budget against the encoded size rather than what the files measure on disk.",[10,2798,2799,2802],{},[894,2800,2801],{},"Do all the attachments need the same MIME type?","\nNo, and they should not. Look up each file's type from its extension so a PDF is sent as a PDF and a CSV as text — a wrong type makes some clients refuse to open the file.",[10,2804,2805,2808,2809,2811],{},[894,2806,2807],{},"Should I zip the attachments?","\nWhen there are many, or the total is large. Excel files compress well because they are already zip containers of XML, though a single ",[14,2810,638],{}," gains little. Some corporate gateways block zip attachments, so check before relying on it.",[10,2813,2814,2817],{},[894,2815,2816],{},"How do I confirm every file was attached?","\nWalk the built message's parts and compare the filenames against what you intended. It is three lines and it catches a path that silently did not exist.",[192,2819,2821],{"id":2820},"related","Related",[2823,2824,2825,2832,2838,2845,2852],"ul",{},[2826,2827,2828,2829,2831],"li",{},"Up to the parent: ",[19,2830,22],{"href":21}," — the connection and single-attachment basics.",[2826,2833,2834,2837],{},[19,2835,2836],{"href":2768},"Email an Excel Report with an HTML Summary Body"," — putting the figures in the message itself.",[2826,2839,2840,2844],{},[19,2841,2843],{"href":2842},"\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 several people safely.",[2826,2846,2847,2851],{},[19,2848,2850],{"href":2849},"\u002Fautomating-reporting-workflows\u002Fgenerating-excel-reports-from-templates\u002Fgenerate-one-excel-report-per-region-in-a-loop\u002F","Generate One Excel Report per Region in a Loop"," — producing the set of files this attaches.",[2826,2853,2854,2857],{},[19,2855,2856],{"href":2763},"Publishing Excel Reports to Cloud Storage"," — the alternative when the set gets large.",[2859,2860,2861],"style",{},"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 .sa561, html code.shiki .sa561{--shiki-default:#E36209;--shiki-dark:#FFB757}html pre.shiki code .sP0c6, html code.shiki .sP0c6{--shiki-default:#005CC5;--shiki-dark:#91CBFF}html pre.shiki code .sSjpA, html code.shiki .sSjpA{--shiki-default:#005CC5;--shiki-dark:#FF9492}html pre.shiki code .s_Opv, html code.shiki .s_Opv{--shiki-default:#6F42C1;--shiki-dark:#DBB7FF}",{"title":202,"searchDepth":252,"depth":252,"links":2863},[2864,2865,2866,2867,2868,2869,2870,2871,2872,2873,2874],{"id":194,"depth":252,"text":195},{"id":408,"depth":252,"text":409},{"id":888,"depth":252,"text":889},{"id":1150,"depth":252,"text":1151},{"id":1353,"depth":252,"text":1354},{"id":1942,"depth":252,"text":1943},{"id":2380,"depth":252,"text":2381},{"id":2500,"depth":252,"text":2501},{"id":2773,"depth":252,"text":2774},{"id":2783,"depth":252,"text":2784},{"id":2820,"depth":252,"text":2821},"2026-08-15","Send several workbooks in one message — loop the attachments with the right MIME type, guard the total size, zip when it grows, and verify every file made it in.","md",[2879,2881,2883,2885,2887],{"q":2789,"a":2880},"Not in the format itself, but every gateway limits the total encoded size, and many recipients find more than a handful unwieldy. Above about five files, zip them or publish and link instead.",{"q":2795,"a":2882},"Roughly a third. Four megabytes of files become about 5.3 megabytes on the wire, so budget against the encoded size rather than what the files measure on disk.",{"q":2801,"a":2884},"No, and they should not. Look up each file's type from its extension so a PDF is sent as a PDF and a CSV as text — a wrong type makes some clients refuse to open the file.",{"q":2807,"a":2886},"When there are many, or the total is large. Excel files compress well because they are already zip containers of XML, though a single .xlsx gains little. Some corporate gateways block zip attachments, so check before relying on it.",{"q":2816,"a":2888},"Walk the built message's parts and compare the filenames against what you intended. It is three lines and it catches a path that silently did not exist.",{},"\u002Fautomating-reporting-workflows\u002Femailing-excel-reports-with-smtplib\u002Fattach-multiple-excel-files-to-one-email-in-python",{"title":2892,"description":2893},"Attach Multiple Excel Files to One Email (Python)","Build an email with several .xlsx attachments using EmailMessage: per-file MIME types, size budgeting with base64 overhead, zipping a large set, and verifying the parts.","attach-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","how-to","rEkDZ341qPdhWRevwi6fpVkV2PNgg_oi-kogbi0blG0",[2899,2902],{"title":22,"path":2900,"stem":2901,"children":-1},"\u002Fautomating-reporting-workflows\u002Femailing-excel-reports-with-smtplib","automating-reporting-workflows\u002Femailing-excel-reports-with-smtplib\u002Findex",{"title":2836,"path":2903,"stem":2904,"children":-1},"\u002Fautomating-reporting-workflows\u002Femailing-excel-reports-with-smtplib\u002Femail-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",1786800028516]