[{"data":1,"prerenderedAt":1978},["ShallowReactive",2],{"doc:\u002Fautomating-reporting-workflows\u002Femailing-excel-reports-with-smtplib\u002Fsend-excel-report-to-multiple-recipients-python":3,"surround:\u002Fautomating-reporting-workflows\u002Femailing-excel-reports-with-smtplib\u002Fsend-excel-report-to-multiple-recipients-python":1970},{"id":4,"title":5,"body":6,"description":1961,"extension":1962,"meta":1963,"navigation":133,"path":1964,"seo":1965,"stem":1968,"__hash__":1969},"docs\u002Fautomating-reporting-workflows\u002Femailing-excel-reports-with-smtplib\u002Fsend-excel-report-to-multiple-recipients-python\u002Findex.md","Send an Excel Report to Multiple Recipients in Python",{"type":7,"value":8,"toc":1948},"minimark",[9,19,24,66,98,102,105,273,277,285,320,335,339,350,811,815,840,992,995,999,1010,1017,1505,1520,1524,1527,1691,1695,1816,1833,1837,1850,1870,1887,1902,1908,1912,1921,1925,1944],[10,11,12,13,18],"p",{},"Sending one Excel report to a single inbox is the easy case. Distributing it to a whole team — or to a list pulled from a spreadsheet — needs care: put the wrong addresses in the wrong header and you leak everyone's email to everyone else, hit a provider rate limit, or let one typo'd address kill the entire batch. This page builds on ",[14,15,17],"a",{"href":16},"\u002Fautomating-reporting-workflows\u002Femailing-excel-reports-with-smtplib\u002F","Emailing Excel Reports with smtplib"," and covers two distinct shapes of multi-recipient delivery: one identical message to many people, and a personalized message per person. Everything uses the standard library only.",[20,21,23],"h2",{"id":22},"what-you-need","What you need",[25,26,27,40,50,60],"ul",{},[28,29,30,34,35,39],"li",{},[31,32,33],"strong",{},"Python 3.6+"," for the ",[36,37,38],"code",{},"EmailMessage"," API.",[28,41,42,45,46,49],{},[31,43,44],{},"SMTP credentials"," — host, port, username, and an ",[31,47,48],{},"app password"," (most providers block your normal account password for scripts).",[28,51,52,59],{},[31,53,54,55,58],{},"An ",[36,56,57],{},".xlsx"," to attach",". The examples generate a tiny one so they stand alone.",[28,61,62,65],{},[31,63,64],{},"A recipient list"," — from an env var, a config value, or a column of the workbook.",[67,68,73],"pre",{"className":69,"code":70,"language":71,"meta":72,"style":72},"language-bash shiki shiki-themes github-light github-dark","pip install pandas openpyxl   # only to generate the sample workbook\n","bash","",[36,74,75],{"__ignoreMap":72},[76,77,80,84,88,91,94],"span",{"class":78,"line":79},"line",1,[76,81,83],{"class":82},"sScJk","pip",[76,85,87],{"class":86},"sZZnC"," install",[76,89,90],{"class":86}," pandas",[76,92,93],{"class":86}," openpyxl",[76,95,97],{"class":96},"sJ8bj","   # only to generate the sample workbook\n",[20,99,101],{"id":100},"build-a-sample-report-and-a-recipient-list","Build a sample report and a recipient list",[10,103,104],{},"The report comes from your upstream job; here it doubles as the source of the recipient column so you can see reading addresses from a sheet.",[67,106,110],{"className":107,"code":108,"language":109,"meta":72,"style":72},"language-python shiki shiki-themes github-light github-dark","import pandas as pd\n\nreport = pd.DataFrame({\n    \"region\": [\"North\", \"South\", \"West\"],\n    \"owner_email\": [\"alice@example.com\", \"bob@example.com\", \"carol@example.com\"],\n    \"revenue\": [159.92, 247.50, 137.44],\n})\nreport.to_excel(\"regional_report.xlsx\", sheet_name=\"Summary\", index=False)\nprint(\"Wrote regional_report.xlsx\")\n","python",[36,111,112,128,135,147,173,196,220,226,259],{"__ignoreMap":72},[76,113,114,118,122,125],{"class":78,"line":79},[76,115,117],{"class":116},"szBVR","import",[76,119,121],{"class":120},"sVt8B"," pandas ",[76,123,124],{"class":116},"as",[76,126,127],{"class":120}," pd\n",[76,129,131],{"class":78,"line":130},2,[76,132,134],{"emptyLinePlaceholder":133},true,"\n",[76,136,138,141,144],{"class":78,"line":137},3,[76,139,140],{"class":120},"report ",[76,142,143],{"class":116},"=",[76,145,146],{"class":120}," pd.DataFrame({\n",[76,148,150,153,156,159,162,165,167,170],{"class":78,"line":149},4,[76,151,152],{"class":86},"    \"region\"",[76,154,155],{"class":120},": [",[76,157,158],{"class":86},"\"North\"",[76,160,161],{"class":120},", ",[76,163,164],{"class":86},"\"South\"",[76,166,161],{"class":120},[76,168,169],{"class":86},"\"West\"",[76,171,172],{"class":120},"],\n",[76,174,176,179,181,184,186,189,191,194],{"class":78,"line":175},5,[76,177,178],{"class":86},"    \"owner_email\"",[76,180,155],{"class":120},[76,182,183],{"class":86},"\"alice@example.com\"",[76,185,161],{"class":120},[76,187,188],{"class":86},"\"bob@example.com\"",[76,190,161],{"class":120},[76,192,193],{"class":86},"\"carol@example.com\"",[76,195,172],{"class":120},[76,197,199,202,204,208,210,213,215,218],{"class":78,"line":198},6,[76,200,201],{"class":86},"    \"revenue\"",[76,203,155],{"class":120},[76,205,207],{"class":206},"sj4cs","159.92",[76,209,161],{"class":120},[76,211,212],{"class":206},"247.50",[76,214,161],{"class":120},[76,216,217],{"class":206},"137.44",[76,219,172],{"class":120},[76,221,223],{"class":78,"line":222},7,[76,224,225],{"class":120},"})\n",[76,227,229,232,235,237,241,243,246,248,251,253,256],{"class":78,"line":228},8,[76,230,231],{"class":120},"report.to_excel(",[76,233,234],{"class":86},"\"regional_report.xlsx\"",[76,236,161],{"class":120},[76,238,240],{"class":239},"s4XuR","sheet_name",[76,242,143],{"class":116},[76,244,245],{"class":86},"\"Summary\"",[76,247,161],{"class":120},[76,249,250],{"class":239},"index",[76,252,143],{"class":116},[76,254,255],{"class":206},"False",[76,257,258],{"class":120},")\n",[76,260,262,265,268,271],{"class":78,"line":261},9,[76,263,264],{"class":206},"print",[76,266,267],{"class":120},"(",[76,269,270],{"class":86},"\"Wrote regional_report.xlsx\"",[76,272,258],{"class":120},[20,274,276],{"id":275},"to-vs-cc-vs-bcc-what-each-reveals","To vs Cc vs Bcc — what each reveals",[10,278,279,280,284],{},"The three address headers route the same way at the envelope level but differ entirely in what recipients ",[281,282,283],"em",{},"see",":",[25,286,287,303,311],{},[28,288,289,292,293,295,296,298,299,302],{},[31,290,291],{},"To"," — primary recipients. Everyone in ",[36,294,291],{}," sees every other ",[36,297,291],{}," and ",[36,300,301],{},"Cc"," address.",[28,304,305,307,308,310],{},[31,306,301],{}," (\"carbon copy\") — also-informed recipients. Visible to all, same as ",[36,309,291],{},".",[28,312,313,316,317,319],{},[31,314,315],{},"Bcc"," (\"blind carbon copy\") — hidden recipients. Each sees only themselves; the ",[36,318,315],{}," header is stripped before transmission.",[10,321,322,323,331,332,334],{},"The rule that matters: ",[31,324,325,326,328,329],{},"never put a distribution list in ",[36,327,291],{}," or ",[36,330,301],{},", or you expose every subscriber's address to the whole list. Use ",[36,333,315],{}," for that.",[20,336,338],{"id":337},"one-message-to-a-visible-team-join-with","One message to a visible team: join with \", \"",[10,340,341,342,345,346,349],{},"For a small team that legitimately should see each other (e.g. three managers cc'd on the same report), set multiple addresses on one header by joining them with ",[36,343,344],{},"\", \"",". ",[36,347,348],{},"send_message()"," parses that into the envelope recipient list automatically.",[67,351,353],{"className":107,"code":352,"language":109,"meta":72,"style":72},"from email.message import EmailMessage\nfrom pathlib import Path\n\ndef build_report_email(sender, to, subject, body, attachment_path, cc=None, bcc=None):\n    \"\"\"Compose an EmailMessage with an .xlsx attachment. No network I\u002FO.\"\"\"\n    path = Path(attachment_path)\n    if not path.is_file() or path.stat().st_size == 0:\n        raise FileNotFoundError(f\"Attachment missing or empty: {path}\")\n\n    msg = EmailMessage()\n    msg[\"From\"] = sender\n    msg[\"To\"] = \", \".join(to)\n    if cc:\n        msg[\"Cc\"] = \", \".join(cc)\n    if bcc:\n        msg[\"Bcc\"] = \", \".join(bcc)\n    msg[\"Subject\"] = subject\n    msg.set_content(body)\n\n    msg.add_attachment(\n        path.read_bytes(),\n        maintype=\"application\",\n        subtype=\"vnd.openxmlformats-officedocument.spreadsheetml.sheet\",\n        filename=path.name,\n    )\n    return msg\n\nmsg = build_report_email(\n    sender=\"reports@example.com\",\n    to=[\"lead@example.com\"],\n    cc=[\"manager@example.com\"],\n    subject=\"Regional Revenue Summary\",\n    body=\"Hi team,\\n\\nThe latest regional report is attached.\\n\\nThanks.\",\n    attachment_path=\"regional_report.xlsx\",\n)\nprint(msg[\"To\"], \"| Cc:\", msg[\"Cc\"])\n",[36,354,355,368,380,384,410,415,425,451,481,485,496,513,531,539,557,565,582,597,603,608,614,620,634,647,658,664,673,678,689,702,718,733,746,770,782,787],{"__ignoreMap":72},[76,356,357,360,363,365],{"class":78,"line":79},[76,358,359],{"class":116},"from",[76,361,362],{"class":120}," email.message ",[76,364,117],{"class":116},[76,366,367],{"class":120}," EmailMessage\n",[76,369,370,372,375,377],{"class":78,"line":130},[76,371,359],{"class":116},[76,373,374],{"class":120}," pathlib ",[76,376,117],{"class":116},[76,378,379],{"class":120}," Path\n",[76,381,382],{"class":78,"line":137},[76,383,134],{"emptyLinePlaceholder":133},[76,385,386,389,392,395,397,400,403,405,407],{"class":78,"line":149},[76,387,388],{"class":116},"def",[76,390,391],{"class":82}," build_report_email",[76,393,394],{"class":120},"(sender, to, subject, body, attachment_path, cc",[76,396,143],{"class":116},[76,398,399],{"class":206},"None",[76,401,402],{"class":120},", bcc",[76,404,143],{"class":116},[76,406,399],{"class":206},[76,408,409],{"class":120},"):\n",[76,411,412],{"class":78,"line":175},[76,413,414],{"class":86},"    \"\"\"Compose an EmailMessage with an .xlsx attachment. No network I\u002FO.\"\"\"\n",[76,416,417,420,422],{"class":78,"line":198},[76,418,419],{"class":120},"    path ",[76,421,143],{"class":116},[76,423,424],{"class":120}," Path(attachment_path)\n",[76,426,427,430,433,436,439,442,445,448],{"class":78,"line":222},[76,428,429],{"class":116},"    if",[76,431,432],{"class":116}," not",[76,434,435],{"class":120}," path.is_file() ",[76,437,438],{"class":116},"or",[76,440,441],{"class":120}," path.stat().st_size ",[76,443,444],{"class":116},"==",[76,446,447],{"class":206}," 0",[76,449,450],{"class":120},":\n",[76,452,453,456,459,461,464,467,470,473,476,479],{"class":78,"line":228},[76,454,455],{"class":116},"        raise",[76,457,458],{"class":206}," FileNotFoundError",[76,460,267],{"class":120},[76,462,463],{"class":116},"f",[76,465,466],{"class":86},"\"Attachment missing or empty: ",[76,468,469],{"class":206},"{",[76,471,472],{"class":120},"path",[76,474,475],{"class":206},"}",[76,477,478],{"class":86},"\"",[76,480,258],{"class":120},[76,482,483],{"class":78,"line":261},[76,484,134],{"emptyLinePlaceholder":133},[76,486,488,491,493],{"class":78,"line":487},10,[76,489,490],{"class":120},"    msg ",[76,492,143],{"class":116},[76,494,495],{"class":120}," EmailMessage()\n",[76,497,499,502,505,508,510],{"class":78,"line":498},11,[76,500,501],{"class":120},"    msg[",[76,503,504],{"class":86},"\"From\"",[76,506,507],{"class":120},"] ",[76,509,143],{"class":116},[76,511,512],{"class":120}," sender\n",[76,514,516,518,521,523,525,528],{"class":78,"line":515},12,[76,517,501],{"class":120},[76,519,520],{"class":86},"\"To\"",[76,522,507],{"class":120},[76,524,143],{"class":116},[76,526,527],{"class":86}," \", \"",[76,529,530],{"class":120},".join(to)\n",[76,532,534,536],{"class":78,"line":533},13,[76,535,429],{"class":116},[76,537,538],{"class":120}," cc:\n",[76,540,542,545,548,550,552,554],{"class":78,"line":541},14,[76,543,544],{"class":120},"        msg[",[76,546,547],{"class":86},"\"Cc\"",[76,549,507],{"class":120},[76,551,143],{"class":116},[76,553,527],{"class":86},[76,555,556],{"class":120},".join(cc)\n",[76,558,560,562],{"class":78,"line":559},15,[76,561,429],{"class":116},[76,563,564],{"class":120}," bcc:\n",[76,566,568,570,573,575,577,579],{"class":78,"line":567},16,[76,569,544],{"class":120},[76,571,572],{"class":86},"\"Bcc\"",[76,574,507],{"class":120},[76,576,143],{"class":116},[76,578,527],{"class":86},[76,580,581],{"class":120},".join(bcc)\n",[76,583,585,587,590,592,594],{"class":78,"line":584},17,[76,586,501],{"class":120},[76,588,589],{"class":86},"\"Subject\"",[76,591,507],{"class":120},[76,593,143],{"class":116},[76,595,596],{"class":120}," subject\n",[76,598,600],{"class":78,"line":599},18,[76,601,602],{"class":120},"    msg.set_content(body)\n",[76,604,606],{"class":78,"line":605},19,[76,607,134],{"emptyLinePlaceholder":133},[76,609,611],{"class":78,"line":610},20,[76,612,613],{"class":120},"    msg.add_attachment(\n",[76,615,617],{"class":78,"line":616},21,[76,618,619],{"class":120},"        path.read_bytes(),\n",[76,621,623,626,628,631],{"class":78,"line":622},22,[76,624,625],{"class":239},"        maintype",[76,627,143],{"class":116},[76,629,630],{"class":86},"\"application\"",[76,632,633],{"class":120},",\n",[76,635,637,640,642,645],{"class":78,"line":636},23,[76,638,639],{"class":239},"        subtype",[76,641,143],{"class":116},[76,643,644],{"class":86},"\"vnd.openxmlformats-officedocument.spreadsheetml.sheet\"",[76,646,633],{"class":120},[76,648,650,653,655],{"class":78,"line":649},24,[76,651,652],{"class":239},"        filename",[76,654,143],{"class":116},[76,656,657],{"class":120},"path.name,\n",[76,659,661],{"class":78,"line":660},25,[76,662,663],{"class":120},"    )\n",[76,665,667,670],{"class":78,"line":666},26,[76,668,669],{"class":116},"    return",[76,671,672],{"class":120}," msg\n",[76,674,676],{"class":78,"line":675},27,[76,677,134],{"emptyLinePlaceholder":133},[76,679,681,684,686],{"class":78,"line":680},28,[76,682,683],{"class":120},"msg ",[76,685,143],{"class":116},[76,687,688],{"class":120}," build_report_email(\n",[76,690,692,695,697,700],{"class":78,"line":691},29,[76,693,694],{"class":239},"    sender",[76,696,143],{"class":116},[76,698,699],{"class":86},"\"reports@example.com\"",[76,701,633],{"class":120},[76,703,705,708,710,713,716],{"class":78,"line":704},30,[76,706,707],{"class":239},"    to",[76,709,143],{"class":116},[76,711,712],{"class":120},"[",[76,714,715],{"class":86},"\"lead@example.com\"",[76,717,172],{"class":120},[76,719,721,724,726,728,731],{"class":78,"line":720},31,[76,722,723],{"class":239},"    cc",[76,725,143],{"class":116},[76,727,712],{"class":120},[76,729,730],{"class":86},"\"manager@example.com\"",[76,732,172],{"class":120},[76,734,736,739,741,744],{"class":78,"line":735},32,[76,737,738],{"class":239},"    subject",[76,740,143],{"class":116},[76,742,743],{"class":86},"\"Regional Revenue Summary\"",[76,745,633],{"class":120},[76,747,749,752,754,757,760,763,765,768],{"class":78,"line":748},33,[76,750,751],{"class":239},"    body",[76,753,143],{"class":116},[76,755,756],{"class":86},"\"Hi team,",[76,758,759],{"class":206},"\\n\\n",[76,761,762],{"class":86},"The latest regional report is attached.",[76,764,759],{"class":206},[76,766,767],{"class":86},"Thanks.\"",[76,769,633],{"class":120},[76,771,773,776,778,780],{"class":78,"line":772},34,[76,774,775],{"class":239},"    attachment_path",[76,777,143],{"class":116},[76,779,234],{"class":86},[76,781,633],{"class":120},[76,783,785],{"class":78,"line":784},35,[76,786,258],{"class":120},[76,788,790,792,795,797,800,803,806,808],{"class":78,"line":789},36,[76,791,264],{"class":206},[76,793,794],{"class":120},"(msg[",[76,796,520],{"class":86},[76,798,799],{"class":120},"], ",[76,801,802],{"class":86},"\"| Cc:\"",[76,804,805],{"class":120},", msg[",[76,807,547],{"class":86},[76,809,810],{"class":120},"])\n",[20,812,814],{"id":813},"one-message-to-a-hidden-distribution-list-bcc","One message to a hidden distribution list (Bcc)",[10,816,817,818,820,821,345,823,825,826,161,828,161,830,833,834,836,837,839],{},"For a newsletter-style blast where recipients must not see each other, leave ",[36,819,291],{}," minimal (often just the sender's own address) and put the list in ",[36,822,315],{},[36,824,348],{}," reads ",[36,827,291],{},[36,829,301],{},[31,831,832],{},"and"," ",[36,835,315],{}," to build the envelope, sends to all of them, then removes the ",[36,838,315],{}," header so it never appears in the delivered message.",[67,841,843],{"className":107,"code":842,"language":109,"meta":72,"style":72},"recipients = [\"alice@example.com\", \"bob@example.com\", \"carol@example.com\"]\n\nmsg = build_report_email(\n    sender=\"reports@example.com\",\n    to=[\"reports@example.com\"],          # the list itself stays hidden\n    bcc=recipients,\n    subject=\"Regional Revenue Summary\",\n    body=\"Report attached.\",\n    attachment_path=\"regional_report.xlsx\",\n)\nprint(\"Bcc count:\", len(msg[\"Bcc\"].split(\",\")))\n\n# Send once; the server fans out to every Bcc address:\n# server.send_message(msg)   # Bcc header is stripped on the wire\n",[36,844,845,868,872,880,890,906,916,926,937,947,951,978,982,987],{"__ignoreMap":72},[76,846,847,850,852,855,857,859,861,863,865],{"class":78,"line":79},[76,848,849],{"class":120},"recipients ",[76,851,143],{"class":116},[76,853,854],{"class":120}," [",[76,856,183],{"class":86},[76,858,161],{"class":120},[76,860,188],{"class":86},[76,862,161],{"class":120},[76,864,193],{"class":86},[76,866,867],{"class":120},"]\n",[76,869,870],{"class":78,"line":130},[76,871,134],{"emptyLinePlaceholder":133},[76,873,874,876,878],{"class":78,"line":137},[76,875,683],{"class":120},[76,877,143],{"class":116},[76,879,688],{"class":120},[76,881,882,884,886,888],{"class":78,"line":149},[76,883,694],{"class":239},[76,885,143],{"class":116},[76,887,699],{"class":86},[76,889,633],{"class":120},[76,891,892,894,896,898,900,903],{"class":78,"line":175},[76,893,707],{"class":239},[76,895,143],{"class":116},[76,897,712],{"class":120},[76,899,699],{"class":86},[76,901,902],{"class":120},"],          ",[76,904,905],{"class":96},"# the list itself stays hidden\n",[76,907,908,911,913],{"class":78,"line":198},[76,909,910],{"class":239},"    bcc",[76,912,143],{"class":116},[76,914,915],{"class":120},"recipients,\n",[76,917,918,920,922,924],{"class":78,"line":222},[76,919,738],{"class":239},[76,921,143],{"class":116},[76,923,743],{"class":86},[76,925,633],{"class":120},[76,927,928,930,932,935],{"class":78,"line":228},[76,929,751],{"class":239},[76,931,143],{"class":116},[76,933,934],{"class":86},"\"Report attached.\"",[76,936,633],{"class":120},[76,938,939,941,943,945],{"class":78,"line":261},[76,940,775],{"class":239},[76,942,143],{"class":116},[76,944,234],{"class":86},[76,946,633],{"class":120},[76,948,949],{"class":78,"line":487},[76,950,258],{"class":120},[76,952,953,955,957,960,962,965,967,969,972,975],{"class":78,"line":498},[76,954,264],{"class":206},[76,956,267],{"class":120},[76,958,959],{"class":86},"\"Bcc count:\"",[76,961,161],{"class":120},[76,963,964],{"class":206},"len",[76,966,794],{"class":120},[76,968,572],{"class":86},[76,970,971],{"class":120},"].split(",[76,973,974],{"class":86},"\",\"",[76,976,977],{"class":120},")))\n",[76,979,980],{"class":78,"line":515},[76,981,134],{"emptyLinePlaceholder":133},[76,983,984],{"class":78,"line":533},[76,985,986],{"class":96},"# Send once; the server fans out to every Bcc address:\n",[76,988,989],{"class":78,"line":541},[76,990,991],{"class":96},"# server.send_message(msg)   # Bcc header is stripped on the wire\n",[10,993,994],{},"One message object, one send, every recipient blind to the others.",[20,996,998],{"id":997},"personalized-separate-emails-over-one-connection","Personalized separate emails over one connection",[10,1000,1001,1002,1005,1006,1009],{},"When each person should get a tailored message (their name, their region's numbers), you must send a ",[281,1003,1004],{},"separate"," message per recipient. The key efficiency point: ",[31,1007,1008],{},"authenticate once and reuse the connection"," for the whole batch instead of reconnecting per message — reconnecting and re-logging-in for every email is slow and trips provider connection limits.",[10,1011,1012,1013,1016],{},"Per-recipient ",[36,1014,1015],{},"try\u002Fexcept"," means a single bad address logs and is skipped rather than aborting the run.",[67,1018,1020],{"className":107,"code":1019,"language":109,"meta":72,"style":72},"import os\nimport smtplib\nfrom email.message import EmailMessage\nfrom pathlib import Path\n\ndef safe_header(value):\n    \"\"\"Strip CR\u002FLF so an untrusted address can't inject extra headers.\"\"\"\n    return value.replace(\"\\r\", \" \").replace(\"\\n\", \" \").strip()\n\ndef build_personalized(sender, recipient, region, attachment_path):\n    path = Path(attachment_path)\n    msg = EmailMessage()\n    msg[\"From\"] = sender\n    msg[\"To\"] = safe_header(recipient)\n    msg[\"Subject\"] = f\"Revenue Summary — {region}\"\n    msg.set_content(f\"Hi,\\n\\nYour {region} report is attached.\\n\\nThanks.\")\n    msg.add_attachment(\n        path.read_bytes(),\n        maintype=\"application\",\n        subtype=\"vnd.openxmlformats-officedocument.spreadsheetml.sheet\",\n        filename=path.name,\n    )\n    return msg\n\ndef send_batch(host, port, username, password, sender, rows, attachment_path):\n    \"\"\"One login, many sends. Returns (sent, failures).\"\"\"\n    sent, failures = [], []\n    with smtplib.SMTP(host, port, timeout=30) as server:\n        server.ehlo()\n        server.starttls()\n        server.ehlo()\n        server.login(username, password)\n        for recipient, region in rows:\n            try:\n                msg = build_personalized(sender, recipient, region, attachment_path)\n                server.send_message(msg)\n                sent.append(recipient)\n            except (smtplib.SMTPRecipientsRefused,\n                    smtplib.SMTPSenderRefused, ValueError) as exc:\n                failures.append((recipient, str(exc)))\n                print(f\"Skipped {recipient!r}: {exc}\")\n    return sent, failures\n\n# Example call (needs real, reachable credentials):\n# rows = [(\"alice@example.com\", \"North\"), (\"bob@example.com\", \"South\")]\n# sent, failures = send_batch(\n#     \"smtp.gmail.com\", 587,\n#     os.environ[\"SMTP_USER\"], os.environ[\"SMTP_PASSWORD\"],\n#     \"reports@example.com\", rows, \"regional_report.xlsx\")\n",[36,1021,1022,1029,1036,1046,1056,1060,1070,1075,1111,1115,1125,1133,1141,1153,1166,1192,1222,1226,1230,1240,1250,1258,1262,1268,1272,1282,1287,1297,1321,1326,1331,1335,1340,1354,1361,1371,1376,1382,1391,1407,1419,1456,1464,1469,1475,1481,1487,1493,1499],{"__ignoreMap":72},[76,1023,1024,1026],{"class":78,"line":79},[76,1025,117],{"class":116},[76,1027,1028],{"class":120}," os\n",[76,1030,1031,1033],{"class":78,"line":130},[76,1032,117],{"class":116},[76,1034,1035],{"class":120}," smtplib\n",[76,1037,1038,1040,1042,1044],{"class":78,"line":137},[76,1039,359],{"class":116},[76,1041,362],{"class":120},[76,1043,117],{"class":116},[76,1045,367],{"class":120},[76,1047,1048,1050,1052,1054],{"class":78,"line":149},[76,1049,359],{"class":116},[76,1051,374],{"class":120},[76,1053,117],{"class":116},[76,1055,379],{"class":120},[76,1057,1058],{"class":78,"line":175},[76,1059,134],{"emptyLinePlaceholder":133},[76,1061,1062,1064,1067],{"class":78,"line":198},[76,1063,388],{"class":116},[76,1065,1066],{"class":82}," safe_header",[76,1068,1069],{"class":120},"(value):\n",[76,1071,1072],{"class":78,"line":222},[76,1073,1074],{"class":86},"    \"\"\"Strip CR\u002FLF so an untrusted address can't inject extra headers.\"\"\"\n",[76,1076,1077,1079,1082,1084,1087,1089,1091,1094,1097,1099,1102,1104,1106,1108],{"class":78,"line":228},[76,1078,669],{"class":116},[76,1080,1081],{"class":120}," value.replace(",[76,1083,478],{"class":86},[76,1085,1086],{"class":206},"\\r",[76,1088,478],{"class":86},[76,1090,161],{"class":120},[76,1092,1093],{"class":86},"\" \"",[76,1095,1096],{"class":120},").replace(",[76,1098,478],{"class":86},[76,1100,1101],{"class":206},"\\n",[76,1103,478],{"class":86},[76,1105,161],{"class":120},[76,1107,1093],{"class":86},[76,1109,1110],{"class":120},").strip()\n",[76,1112,1113],{"class":78,"line":261},[76,1114,134],{"emptyLinePlaceholder":133},[76,1116,1117,1119,1122],{"class":78,"line":487},[76,1118,388],{"class":116},[76,1120,1121],{"class":82}," build_personalized",[76,1123,1124],{"class":120},"(sender, recipient, region, attachment_path):\n",[76,1126,1127,1129,1131],{"class":78,"line":498},[76,1128,419],{"class":120},[76,1130,143],{"class":116},[76,1132,424],{"class":120},[76,1134,1135,1137,1139],{"class":78,"line":515},[76,1136,490],{"class":120},[76,1138,143],{"class":116},[76,1140,495],{"class":120},[76,1142,1143,1145,1147,1149,1151],{"class":78,"line":533},[76,1144,501],{"class":120},[76,1146,504],{"class":86},[76,1148,507],{"class":120},[76,1150,143],{"class":116},[76,1152,512],{"class":120},[76,1154,1155,1157,1159,1161,1163],{"class":78,"line":541},[76,1156,501],{"class":120},[76,1158,520],{"class":86},[76,1160,507],{"class":120},[76,1162,143],{"class":116},[76,1164,1165],{"class":120}," safe_header(recipient)\n",[76,1167,1168,1170,1172,1174,1176,1179,1182,1184,1187,1189],{"class":78,"line":559},[76,1169,501],{"class":120},[76,1171,589],{"class":86},[76,1173,507],{"class":120},[76,1175,143],{"class":116},[76,1177,1178],{"class":116}," f",[76,1180,1181],{"class":86},"\"Revenue Summary — ",[76,1183,469],{"class":206},[76,1185,1186],{"class":120},"region",[76,1188,475],{"class":206},[76,1190,1191],{"class":86},"\"\n",[76,1193,1194,1197,1199,1202,1204,1207,1209,1211,1213,1216,1218,1220],{"class":78,"line":567},[76,1195,1196],{"class":120},"    msg.set_content(",[76,1198,463],{"class":116},[76,1200,1201],{"class":86},"\"Hi,",[76,1203,759],{"class":206},[76,1205,1206],{"class":86},"Your ",[76,1208,469],{"class":206},[76,1210,1186],{"class":120},[76,1212,475],{"class":206},[76,1214,1215],{"class":86}," report is attached.",[76,1217,759],{"class":206},[76,1219,767],{"class":86},[76,1221,258],{"class":120},[76,1223,1224],{"class":78,"line":584},[76,1225,613],{"class":120},[76,1227,1228],{"class":78,"line":599},[76,1229,619],{"class":120},[76,1231,1232,1234,1236,1238],{"class":78,"line":605},[76,1233,625],{"class":239},[76,1235,143],{"class":116},[76,1237,630],{"class":86},[76,1239,633],{"class":120},[76,1241,1242,1244,1246,1248],{"class":78,"line":610},[76,1243,639],{"class":239},[76,1245,143],{"class":116},[76,1247,644],{"class":86},[76,1249,633],{"class":120},[76,1251,1252,1254,1256],{"class":78,"line":616},[76,1253,652],{"class":239},[76,1255,143],{"class":116},[76,1257,657],{"class":120},[76,1259,1260],{"class":78,"line":622},[76,1261,663],{"class":120},[76,1263,1264,1266],{"class":78,"line":636},[76,1265,669],{"class":116},[76,1267,672],{"class":120},[76,1269,1270],{"class":78,"line":649},[76,1271,134],{"emptyLinePlaceholder":133},[76,1273,1274,1276,1279],{"class":78,"line":660},[76,1275,388],{"class":116},[76,1277,1278],{"class":82}," send_batch",[76,1280,1281],{"class":120},"(host, port, username, password, sender, rows, attachment_path):\n",[76,1283,1284],{"class":78,"line":666},[76,1285,1286],{"class":86},"    \"\"\"One login, many sends. Returns (sent, failures).\"\"\"\n",[76,1288,1289,1292,1294],{"class":78,"line":675},[76,1290,1291],{"class":120},"    sent, failures ",[76,1293,143],{"class":116},[76,1295,1296],{"class":120}," [], []\n",[76,1298,1299,1302,1305,1308,1310,1313,1316,1318],{"class":78,"line":680},[76,1300,1301],{"class":116},"    with",[76,1303,1304],{"class":120}," smtplib.SMTP(host, port, ",[76,1306,1307],{"class":239},"timeout",[76,1309,143],{"class":116},[76,1311,1312],{"class":206},"30",[76,1314,1315],{"class":120},") ",[76,1317,124],{"class":116},[76,1319,1320],{"class":120}," server:\n",[76,1322,1323],{"class":78,"line":691},[76,1324,1325],{"class":120},"        server.ehlo()\n",[76,1327,1328],{"class":78,"line":704},[76,1329,1330],{"class":120},"        server.starttls()\n",[76,1332,1333],{"class":78,"line":720},[76,1334,1325],{"class":120},[76,1336,1337],{"class":78,"line":735},[76,1338,1339],{"class":120},"        server.login(username, password)\n",[76,1341,1342,1345,1348,1351],{"class":78,"line":748},[76,1343,1344],{"class":116},"        for",[76,1346,1347],{"class":120}," recipient, region ",[76,1349,1350],{"class":116},"in",[76,1352,1353],{"class":120}," rows:\n",[76,1355,1356,1359],{"class":78,"line":772},[76,1357,1358],{"class":116},"            try",[76,1360,450],{"class":120},[76,1362,1363,1366,1368],{"class":78,"line":784},[76,1364,1365],{"class":120},"                msg ",[76,1367,143],{"class":116},[76,1369,1370],{"class":120}," build_personalized(sender, recipient, region, attachment_path)\n",[76,1372,1373],{"class":78,"line":789},[76,1374,1375],{"class":120},"                server.send_message(msg)\n",[76,1377,1379],{"class":78,"line":1378},37,[76,1380,1381],{"class":120},"                sent.append(recipient)\n",[76,1383,1385,1388],{"class":78,"line":1384},38,[76,1386,1387],{"class":116},"            except",[76,1389,1390],{"class":120}," (smtplib.SMTPRecipientsRefused,\n",[76,1392,1394,1397,1400,1402,1404],{"class":78,"line":1393},39,[76,1395,1396],{"class":120},"                    smtplib.SMTPSenderRefused, ",[76,1398,1399],{"class":206},"ValueError",[76,1401,1315],{"class":120},[76,1403,124],{"class":116},[76,1405,1406],{"class":120}," exc:\n",[76,1408,1410,1413,1416],{"class":78,"line":1409},40,[76,1411,1412],{"class":120},"                failures.append((recipient, ",[76,1414,1415],{"class":206},"str",[76,1417,1418],{"class":120},"(exc)))\n",[76,1420,1422,1425,1427,1429,1432,1434,1437,1440,1442,1445,1447,1450,1452,1454],{"class":78,"line":1421},41,[76,1423,1424],{"class":206},"                print",[76,1426,267],{"class":120},[76,1428,463],{"class":116},[76,1430,1431],{"class":86},"\"Skipped ",[76,1433,469],{"class":206},[76,1435,1436],{"class":120},"recipient",[76,1438,1439],{"class":116},"!r",[76,1441,475],{"class":206},[76,1443,1444],{"class":86},": ",[76,1446,469],{"class":206},[76,1448,1449],{"class":120},"exc",[76,1451,475],{"class":206},[76,1453,478],{"class":86},[76,1455,258],{"class":120},[76,1457,1459,1461],{"class":78,"line":1458},42,[76,1460,669],{"class":116},[76,1462,1463],{"class":120}," sent, failures\n",[76,1465,1467],{"class":78,"line":1466},43,[76,1468,134],{"emptyLinePlaceholder":133},[76,1470,1472],{"class":78,"line":1471},44,[76,1473,1474],{"class":96},"# Example call (needs real, reachable credentials):\n",[76,1476,1478],{"class":78,"line":1477},45,[76,1479,1480],{"class":96},"# rows = [(\"alice@example.com\", \"North\"), (\"bob@example.com\", \"South\")]\n",[76,1482,1484],{"class":78,"line":1483},46,[76,1485,1486],{"class":96},"# sent, failures = send_batch(\n",[76,1488,1490],{"class":78,"line":1489},47,[76,1491,1492],{"class":96},"#     \"smtp.gmail.com\", 587,\n",[76,1494,1496],{"class":78,"line":1495},48,[76,1497,1498],{"class":96},"#     os.environ[\"SMTP_USER\"], os.environ[\"SMTP_PASSWORD\"],\n",[76,1500,1502],{"class":78,"line":1501},49,[76,1503,1504],{"class":96},"#     \"reports@example.com\", rows, \"regional_report.xlsx\")\n",[10,1506,1507,1508,1511,1512,1515,1516,1519],{},"Catching ",[36,1509,1510],{},"SMTPRecipientsRefused"," and friends ",[281,1513,1514],{},"inside"," the loop is what keeps one rejected address from terminating the batch. Connection-level failures (a dropped socket, auth failure) still raise out of the ",[36,1517,1518],{},"with"," block, as they should — there's no point continuing a batch with no connection.",[20,1521,1523],{"id":1522},"reading-the-recipient-list-from-env-or-the-spreadsheet","Reading the recipient list from env or the spreadsheet",[10,1525,1526],{},"Hard-coding addresses doesn't scale. Pull them from a comma-separated env var for a fixed list, or from a column of the workbook for a data-driven one.",[67,1528,1530],{"className":107,"code":1529,"language":109,"meta":72,"style":72},"import os\nimport pandas as pd\n\n# From an env var: SMTP_RECIPIENTS=\"alice@example.com,bob@example.com\"\nenv_list = [a.strip() for a in os.getenv(\"SMTP_RECIPIENTS\", \"\").split(\",\") if a.strip()]\n\n# From a spreadsheet column produced upstream:\ndf = pd.read_excel(\"regional_report.xlsx\", sheet_name=\"Summary\")\nrows = list(zip(df[\"owner_email\"], df[\"region\"]))   # for personalized sends\n\nprint(\"env recipients:\", env_list)\nprint(\"sheet rows:\", rows)\n",[36,1531,1532,1538,1548,1552,1557,1599,1603,1608,1630,1663,1667,1679],{"__ignoreMap":72},[76,1533,1534,1536],{"class":78,"line":79},[76,1535,117],{"class":116},[76,1537,1028],{"class":120},[76,1539,1540,1542,1544,1546],{"class":78,"line":130},[76,1541,117],{"class":116},[76,1543,121],{"class":120},[76,1545,124],{"class":116},[76,1547,127],{"class":120},[76,1549,1550],{"class":78,"line":137},[76,1551,134],{"emptyLinePlaceholder":133},[76,1553,1554],{"class":78,"line":149},[76,1555,1556],{"class":96},"# From an env var: SMTP_RECIPIENTS=\"alice@example.com,bob@example.com\"\n",[76,1558,1559,1562,1564,1567,1570,1573,1575,1578,1581,1583,1586,1589,1591,1593,1596],{"class":78,"line":175},[76,1560,1561],{"class":120},"env_list ",[76,1563,143],{"class":116},[76,1565,1566],{"class":120}," [a.strip() ",[76,1568,1569],{"class":116},"for",[76,1571,1572],{"class":120}," a ",[76,1574,1350],{"class":116},[76,1576,1577],{"class":120}," os.getenv(",[76,1579,1580],{"class":86},"\"SMTP_RECIPIENTS\"",[76,1582,161],{"class":120},[76,1584,1585],{"class":86},"\"\"",[76,1587,1588],{"class":120},").split(",[76,1590,974],{"class":86},[76,1592,1315],{"class":120},[76,1594,1595],{"class":116},"if",[76,1597,1598],{"class":120}," a.strip()]\n",[76,1600,1601],{"class":78,"line":198},[76,1602,134],{"emptyLinePlaceholder":133},[76,1604,1605],{"class":78,"line":222},[76,1606,1607],{"class":96},"# From a spreadsheet column produced upstream:\n",[76,1609,1610,1613,1615,1618,1620,1622,1624,1626,1628],{"class":78,"line":228},[76,1611,1612],{"class":120},"df ",[76,1614,143],{"class":116},[76,1616,1617],{"class":120}," pd.read_excel(",[76,1619,234],{"class":86},[76,1621,161],{"class":120},[76,1623,240],{"class":239},[76,1625,143],{"class":116},[76,1627,245],{"class":86},[76,1629,258],{"class":120},[76,1631,1632,1635,1637,1640,1642,1645,1648,1651,1654,1657,1660],{"class":78,"line":261},[76,1633,1634],{"class":120},"rows ",[76,1636,143],{"class":116},[76,1638,1639],{"class":206}," list",[76,1641,267],{"class":120},[76,1643,1644],{"class":206},"zip",[76,1646,1647],{"class":120},"(df[",[76,1649,1650],{"class":86},"\"owner_email\"",[76,1652,1653],{"class":120},"], df[",[76,1655,1656],{"class":86},"\"region\"",[76,1658,1659],{"class":120},"]))   ",[76,1661,1662],{"class":96},"# for personalized sends\n",[76,1664,1665],{"class":78,"line":487},[76,1666,134],{"emptyLinePlaceholder":133},[76,1668,1669,1671,1673,1676],{"class":78,"line":498},[76,1670,264],{"class":206},[76,1672,267],{"class":120},[76,1674,1675],{"class":86},"\"env recipients:\"",[76,1677,1678],{"class":120},", env_list)\n",[76,1680,1681,1683,1685,1688],{"class":78,"line":515},[76,1682,264],{"class":206},[76,1684,267],{"class":120},[76,1686,1687],{"class":86},"\"sheet rows:\"",[76,1689,1690],{"class":120},", rows)\n",[20,1692,1694],{"id":1693},"common-pitfalls-and-fixes","Common pitfalls and fixes",[1696,1697,1698,1714],"table",{},[1699,1700,1701],"thead",{},[1702,1703,1704,1708,1711],"tr",{},[1705,1706,1707],"th",{},"Error \u002F symptom",[1705,1709,1710],{},"Cause",[1705,1712,1713],{},"Fix",[1715,1716,1717,1739,1755,1777,1798],"tbody",{},[1702,1718,1719,1723,1730],{},[1720,1721,1722],"td",{},"Recipients see everyone's address",[1720,1724,1725,1726,328,1728],{},"Whole list placed in ",[36,1727,291],{},[36,1729,301],{},[1720,1731,1732,1733,1735,1736,1738],{},"Put the distribution list in ",[36,1734,315],{},"; keep ",[36,1737,291],{}," to the sender or a single alias.",[1702,1740,1741,1746,1749],{},[1720,1742,1743,1745],{},[36,1744,1510],{}," aborts the run",[1720,1747,1748],{},"One bad address raised and was uncaught in a loop",[1720,1750,1751,1752,1754],{},"Wrap each send in ",[36,1753,1015],{}," so a single failure is logged and skipped.",[1702,1756,1757,1767,1770],{},[1720,1758,1759,1762,1763,1766],{},[36,1760,1761],{},"421"," \u002F ",[36,1764,1765],{},"Too many messages"," \u002F throttling",[1720,1768,1769],{},"Provider per-message or per-connection rate limit",[1720,1771,1772,1773,1776],{},"Send in batches with a short ",[36,1774,1775],{},"time.sleep()"," between sends; check your provider's documented daily and hourly caps.",[1702,1778,1779,1782,1785],{},[1720,1780,1781],{},"Slow batch, intermittent disconnects",[1720,1783,1784],{},"Reconnecting and logging in per message",[1720,1786,1787,1788,1791,1792,1795,1796,310],{},"Open one ",[36,1789,1790],{},"smtplib.SMTP"," connection, ",[36,1793,1794],{},"login()"," once, and reuse it for every ",[36,1797,348],{},[1702,1799,1800,1805,1808],{},[1720,1801,1802,1803],{},"Unexpected extra headers \u002F ",[36,1804,1399],{},[1720,1806,1807],{},"CR\u002FLF (header injection) in an untrusted address",[1720,1809,1810,1811,298,1813,1815],{},"Strip ",[36,1812,1086],{},[36,1814,1101],{}," from any address or subject built from external data before setting the header.",[10,1817,1818,1819,1822,1823,1826,1827,1829,1830,1832],{},"A short note on personalization vs Bcc: they are mutually exclusive strategies. Bcc sends ",[281,1820,1821],{},"one"," message to many; the loop sends ",[281,1824,1825],{},"many"," messages, one each. Don't mix them — a personalized message in ",[36,1828,291],{}," with the whole list also in ",[36,1831,315],{}," exposes nothing but defeats the personalization and inflates your recipient count.",[20,1834,1836],{"id":1835},"frequently-asked-questions","Frequently asked questions",[10,1838,1839,1842,1843,1846,1847,1849],{},[31,1840,1841],{},"Should I use Bcc or a loop for a 500-person list?"," Bcc, in chunks. A loop is for genuinely ",[281,1844,1845],{},"different"," content per recipient. For identical content, Bcc is far fewer messages — but split a large ",[36,1848,315],{}," into batches of, say, 50–100 to stay under per-message recipient caps, sending each batch as its own message.",[10,1851,1852,1858,1859,161,1861,1863,1864,1866,1867,1869],{},[31,1853,1854,1855,1857],{},"Does ",[36,1856,348],{}," really strip the Bcc header?"," Yes. It reads ",[36,1860,291],{},[36,1862,301],{},", and ",[36,1865,315],{}," to compute the envelope recipients, then removes ",[36,1868,315],{}," so the delivered message never reveals the hidden list. You don't pass recipients separately — the headers are the source of truth.",[10,1871,1872,1875,1876,1879,1880,1879,1883,1886],{},[31,1873,1874],{},"Why reuse one SMTP connection instead of reconnecting?"," Each ",[36,1877,1878],{},"connect","\u002F",[36,1881,1882],{},"starttls",[36,1884,1885],{},"login"," cycle is a full TLS handshake plus authentication round-trips — slow, and many providers limit how often you may connect. One connection for the whole batch is dramatically faster and friendlier to rate limits.",[10,1888,1889,1892,1893,1879,1895,1897,1898,1901],{},[31,1890,1891],{},"How do I avoid header injection from spreadsheet addresses?"," Treat any externally sourced address as untrusted and strip ",[36,1894,1086],{},[36,1896,1101],{}," before assigning it to a header (the ",[36,1899,1900],{},"safe_header"," helper above). A newline smuggled into an address could otherwise add arbitrary headers to your message.",[10,1903,1904,1907],{},[31,1905,1906],{},"What if I hit a daily send limit mid-batch?"," Catch the connection-level error, record which recipients were already sent, and resume from there on the next run. Persisting a \"sent\" set lets you make batches idempotent across retries.",[20,1909,1911],{"id":1910},"conclusion","Conclusion",[10,1913,1914,1915,1917,1918,1920],{},"Multi-recipient delivery comes down to picking the right tool for the shape of the job. For identical content where readers shouldn't see each other, build one message and put the list in ",[36,1916,315],{}," — ",[36,1919,348],{}," handles the envelope and strips the header. For tailored content, loop over the recipients, build a distinct message each time, and crucially reuse a single authenticated connection while catching per-recipient errors so one bad address can't sink the batch. Read your list from an env var or a spreadsheet column, sanitize untrusted addresses, and respect your provider's recipient and rate limits.",[20,1922,1924],{"id":1923},"where-to-go-next","Where to go next",[10,1926,1927,1928,1930,1931,1935,1936,298,1940,310],{},"Return to ",[14,1929,17],{"href":16}," for the message-construction and secure-connection fundamentals these examples build on. To run the whole generate-then-send pipeline unattended, see ",[14,1932,1934],{"href":1933},"\u002Fautomating-reporting-workflows\u002Fscheduling-python-excel-scripts-with-cron\u002F","Scheduling Python Excel Scripts with Cron",". And for producing the workbooks you distribute, see ",[14,1937,1939],{"href":1938},"\u002Fautomating-reporting-workflows\u002Fbuilding-multi-sheet-excel-dashboards\u002F","Building Multi-Sheet Excel Dashboards",[14,1941,1943],{"href":1942},"\u002Fgetting-started-with-python-excel-automation\u002Fwriting-dataframes-to-excel-with-pandas\u002F","Writing DataFrames to Excel with pandas",[1945,1946,1947],"style",{},"html pre.shiki code .sScJk, html code.shiki .sScJk{--shiki-default:#6F42C1;--shiki-dark:#B392F0}html pre.shiki code .sZZnC, html code.shiki .sZZnC{--shiki-default:#032F62;--shiki-dark:#9ECBFF}html pre.shiki code .sJ8bj, html code.shiki .sJ8bj{--shiki-default:#6A737D;--shiki-dark:#6A737D}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 .szBVR, html code.shiki .szBVR{--shiki-default:#D73A49;--shiki-dark:#F97583}html pre.shiki code .sVt8B, html code.shiki .sVt8B{--shiki-default:#24292E;--shiki-dark:#E1E4E8}html pre.shiki code .sj4cs, html code.shiki .sj4cs{--shiki-default:#005CC5;--shiki-dark:#79B8FF}html pre.shiki code .s4XuR, html code.shiki .s4XuR{--shiki-default:#E36209;--shiki-dark:#FFAB70}",{"title":72,"searchDepth":130,"depth":130,"links":1949},[1950,1951,1952,1953,1954,1955,1956,1957,1958,1959,1960],{"id":22,"depth":130,"text":23},{"id":100,"depth":130,"text":101},{"id":275,"depth":130,"text":276},{"id":337,"depth":130,"text":338},{"id":813,"depth":130,"text":814},{"id":997,"depth":130,"text":998},{"id":1522,"depth":130,"text":1523},{"id":1693,"depth":130,"text":1694},{"id":1835,"depth":130,"text":1836},{"id":1910,"depth":130,"text":1911},{"id":1923,"depth":130,"text":1924},"Email one .xlsx report to many recipients in Python: To vs Cc vs Bcc, hidden distribution lists, personalized per-recipient sends over one SMTP connection.","md",{},"\u002Fautomating-reporting-workflows\u002Femailing-excel-reports-with-smtplib\u002Fsend-excel-report-to-multiple-recipients-python",{"title":1966,"description":1967},"Email Excel to Multiple Recipients in Python","Send an Excel report to many recipients with Python's stdlib: To\u002FCc\u002FBcc, a hidden Bcc distribution list, and personalized loop sends over one reused connection.","automating-reporting-workflows\u002Femailing-excel-reports-with-smtplib\u002Fsend-excel-report-to-multiple-recipients-python\u002Findex","hY0gTPz2rD1iI0cpDmIyhWVvx2hWyBSuQCBjTKbC2qQ",[1971,1974],{"title":17,"path":1972,"stem":1973,"children":-1},"\u002Fautomating-reporting-workflows\u002Femailing-excel-reports-with-smtplib","automating-reporting-workflows\u002Femailing-excel-reports-with-smtplib\u002Findex",{"title":1975,"path":1976,"stem":1977,"children":-1},"Exporting Excel Reports to PDF","\u002Fautomating-reporting-workflows\u002Fexporting-excel-reports-to-pdf","automating-reporting-workflows\u002Fexporting-excel-reports-to-pdf\u002Findex",1781773160865]