[{"data":1,"prerenderedAt":2170},["ShallowReactive",2],{"doc:\u002Fautomating-reporting-workflows\u002Femailing-excel-reports-with-smtplib\u002Fsend-an-excel-report-with-amazon-ses":3,"surround:\u002Fautomating-reporting-workflows\u002Femailing-excel-reports-with-smtplib\u002Fsend-an-excel-report-with-amazon-ses":2137,"ancestors:\u002Fautomating-reporting-workflows\u002Femailing-excel-reports-with-smtplib\u002Fsend-an-excel-report-with-amazon-ses":2146,"children:\u002Fautomating-reporting-workflows\u002Femailing-excel-reports-with-smtplib\u002Fsend-an-excel-report-with-amazon-ses":2151,"related:\u002Fautomating-reporting-workflows\u002Femailing-excel-reports-with-smtplib\u002Fsend-an-excel-report-with-amazon-ses":2152},{"id":4,"title":5,"body":6,"dateModified":2104,"datePublished":2104,"description":2105,"extension":2106,"faq":2107,"keyword":2116,"kind":2117,"meta":2118,"navigation":279,"path":2126,"seo":2127,"slug":2130,"stem":2131,"topics":2132,"type":2135,"__hash__":2136},"docs\u002Fautomating-reporting-workflows\u002Femailing-excel-reports-with-smtplib\u002Fsend-an-excel-report-with-amazon-ses\u002Findex.md","Send an Excel Report with Amazon SES",{"type":7,"value":8,"toc":2092},"minimark",[9,29,34,79,86,215,219,225,681,764,779,783,798,1040,1066,1070,1077,1387,1399,1562,1566,1569,1692,1705,1709,1712,1948,1966,1970,2014,2018,2027,2031,2037,2043,2049,2055,2059,2088],[10,11,12,13,17,18,22,23,28],"p",{},"When a reporting job already runs on AWS — in Lambda, ECS or on an EC2 instance — Amazon SES is the natural way to send its output. There is no mail server to run, the credentials come from the job's IAM role rather than a password in a config file, and deliverability features such as DKIM signing are handled by the service. SES's simple API sends text and HTML bodies only; attachments go through the ",[14,15,16],"em",{},"raw"," message format, where you build the complete MIME email yourself and SES delivers it as is. Python's standard ",[19,20,21],"code",{},"email"," package builds that message, and boto3 sends it. This page, part of ",[24,25,27],"a",{"href":26},"\u002Fautomating-reporting-workflows\u002Femailing-excel-reports-with-smtplib\u002F","Emailing Excel Reports with smtplib",", covers both, plus the errors SES actually returns and a way to test everything locally.",[30,31,33],"h2",{"id":32},"prerequisites","Prerequisites",[35,36,41],"pre",{"className":37,"code":38,"language":39,"meta":40,"style":40},"language-bash shiki shiki-themes github-light github-dark-high-contrast","pip install boto3 pandas openpyxl\npip install \"moto[ses]\"          # only for the local tests\n","bash","",[19,42,43,65],{"__ignoreMap":40},[44,45,48,52,56,59,62],"span",{"class":46,"line":47},"line",1,[44,49,51],{"class":50},"sMTad","pip",[44,53,55],{"class":54},"srMev"," install",[44,57,58],{"class":54}," boto3",[44,60,61],{"class":54}," pandas",[44,63,64],{"class":54}," openpyxl\n",[44,66,68,70,72,75],{"class":46,"line":67},2,[44,69,51],{"class":50},[44,71,55],{"class":54},[44,73,74],{"class":54}," \"moto[ses]\"",[44,76,78],{"class":77},"s-wDw","          # only for the local tests\n",[10,80,81,82,85],{},"In AWS you need a verified sender identity in SES — a domain, or a single email address — and permission to send through SES for the credentials the script runs with. New SES accounts start in the ",[14,83,84],{},"sandbox",", where mail can only go to verified addresses and the daily quota is small; production access is requested per region. The examples run against moto, a library that imitates AWS services in memory, so they work without an account.",[87,88,96,97,96,101,96,105,96,113,96,123,96,130,96,135,96,140,96,143,96,147,96,152,96,154,96,156,96,161,96,166,96,174,96,180,96,185,96,188,96,193,96,197,96,202,96,205,96,210],"svg",{"viewBox":89,"role":90,"ariaLabelledBy":91,"xmlns":94,"style":95},"4 0 752 244","img",[92,93],"ses-seq-t","ses-seq-d","http:\u002F\u002Fwww.w3.org\u002F2000\u002Fsvg","width:100%;max-width:752px;height:auto;display:block;margin:1.5rem auto;font-family:Inter,ui-sans-serif,system-ui,sans-serif","\n  ",[98,99,100],"title",{"id":92},"What happens when the job sends a report",[102,103,104],"desc",{"id":93},"A sequence diagram. The report job builds a MIME message with the workbook attached and calls SES SendEmail with raw content; SES checks the sender identity and sending quota, signs and delivers the message, and returns a message ID.",[106,107],"rect",{"x":108,"y":109,"width":110,"height":111,"fill":112},"4","0","752","244","#ffffff",[106,114],{"x":115,"y":116,"width":117,"height":118,"rx":119,"fill":120,"stroke":121,"style":122},"20.0","16.0","180.0","48.0","12","#f0f4ff","var(--brand,#5b5cf0)","stroke-width:2px",[124,125,129],"text",{"x":126,"y":127,"style":128},"110.0","44.5","font-size:13px;font-weight:700;fill:var(--brand-strong,#4338ca);text-anchor:middle","report job",[106,131],{"x":132,"y":116,"width":117,"height":118,"rx":119,"fill":133,"stroke":134,"style":122},"290.0","#e7ebef","var(--line,#cdd5e6)",[124,136,139],{"x":137,"y":127,"style":138},"380.0","font-size:13px;font-weight:700;fill:var(--text,#172033);text-anchor:middle","Amazon SES",[106,141],{"x":142,"y":116,"width":117,"height":118,"rx":119,"fill":133,"stroke":134,"style":122},"560.0",[124,144,146],{"x":145,"y":127,"style":138},"650.0","recipient",[46,148],{"x1":126,"y1":149,"x2":126,"y2":150,"stroke":134,"style":151},"64.0","228.0","stroke-width:1.5px;stroke-dasharray:5 4",[46,153],{"x1":137,"y1":149,"x2":137,"y2":150,"stroke":134,"style":151},[46,155],{"x1":145,"y1":149,"x2":145,"y2":150,"stroke":134,"style":151},[46,157],{"x1":158,"y1":159,"x2":160,"y2":159,"stroke":121,"style":122},"115.0","104.0","371.0",[162,163],"polygon",{"points":164,"fill":165},"375.0,104.0 366.0,109.0 366.0,99.0","#5b5cf0",[106,167],{"x":168,"y":169,"width":170,"height":171,"rx":172,"fill":112,"stroke":134,"style":173},"140.1","75.0","209.8","21.0","10","stroke-width:1.2px",[124,175,179],{"x":176,"y":177,"style":178},"245.0","89.5","font-size:11.5px;font-weight:700;fill:var(--text,#172033);text-anchor:middle","1. SendEmail(Raw = MIME bytes)",[46,181],{"x1":182,"y1":183,"x2":184,"y2":183,"stroke":121,"style":122},"375.0","152.0","119.0",[162,186],{"points":187,"fill":165},"115.0,152.0 124.0,147.0 124.0,157.0",[106,189],{"x":190,"y":191,"width":192,"height":171,"rx":172,"fill":112,"stroke":134,"style":173},"129.9","123.0","230.2",[124,194,196],{"x":176,"y":195,"style":178},"137.5","2. MessageId (or MessageRejected)",[46,198],{"x1":199,"y1":200,"x2":201,"y2":200,"stroke":121,"style":122},"385.0","200.0","641.0",[162,203],{"points":204,"fill":165},"645.0,200.0 636.0,205.0 636.0,195.0",[106,206],{"x":207,"y":208,"width":209,"height":171,"rx":172,"fill":112,"stroke":134,"style":173},"405.1","171.0","219.8",[124,211,214],{"x":212,"y":213,"style":178},"515.0","185.5","3. signed email + .xlsx attachment",[30,216,218],{"id":217},"step-1-build-the-message-with-the-workbook-attached","Step 1 — Build the message with the workbook attached",[10,220,221,224],{},[19,222,223],{},"EmailMessage"," from the standard library produces a correct multipart message: a plain-text body, an HTML alternative, and the attachment with the right content type:",[35,226,230],{"className":227,"code":228,"language":229,"meta":40,"style":40},"language-python shiki shiki-themes github-light github-dark-high-contrast","from email.message import EmailMessage\nfrom pathlib import Path\nimport pandas as pd\n\nXLSX_TYPE = (\"application\", \"vnd.openxmlformats-officedocument.spreadsheetml.sheet\")\n\ndef build_message(sender, recipients, subject, report_path):\n    df = pd.read_excel(report_path)\n    below = df[df[\"Sales\"] \u003C df[\"Target\"]][\"Region\"].tolist()\n    msg = EmailMessage()\n    msg[\"Subject\"] = subject\n    msg[\"From\"] = sender\n    msg[\"To\"] = \", \".join(recipients)\n    msg.set_content(f\"Weekly sales attached. Below target: {', '.join(below) or 'none'}.\")\n    msg.add_alternative(\n        f\"\u003Cp>Weekly sales attached.\u003C\u002Fp>\u003Cp>\u003Cb>Below target:\u003C\u002Fb> {', '.join(below) or 'none'}\u003C\u002Fp>\"\n        + df.to_html(index=False, border=0), subtype=\"html\")\n    msg.add_attachment(Path(report_path).read_bytes(), maintype=XLSX_TYPE[0], subtype=XLSX_TYPE[1],\n                       filename=Path(report_path).name)\n    return msg\n\nmsg = build_message(\"reports@example.com\", [\"sales-team@example.org\"], \"Weekly sales — week 39\", \"weekly_sales.xlsx\")\nprint(msg.get_content_type(), [part.get_content_type() for part in msg.iter_parts()])\nprint(f\"{len(msg.as_bytes()):,} bytes\")\n","python",[19,231,232,248,260,274,281,306,311,324,336,371,382,398,413,431,467,473,497,537,572,583,592,597,629,650],{"__ignoreMap":40},[44,233,234,238,242,245],{"class":46,"line":47},[44,235,237],{"class":236},"s-kum","from",[44,239,241],{"class":240},"skGVy"," email.message ",[44,243,244],{"class":236},"import",[44,246,247],{"class":240}," EmailMessage\n",[44,249,250,252,255,257],{"class":46,"line":67},[44,251,237],{"class":236},[44,253,254],{"class":240}," pathlib ",[44,256,244],{"class":236},[44,258,259],{"class":240}," Path\n",[44,261,263,265,268,271],{"class":46,"line":262},3,[44,264,244],{"class":236},[44,266,267],{"class":240}," pandas ",[44,269,270],{"class":236},"as",[44,272,273],{"class":240}," pd\n",[44,275,277],{"class":46,"line":276},4,[44,278,280],{"emptyLinePlaceholder":279},true,"\n",[44,282,284,288,291,294,297,300,303],{"class":46,"line":283},5,[44,285,287],{"class":286},"sP0c6","XLSX_TYPE",[44,289,290],{"class":236}," =",[44,292,293],{"class":240}," (",[44,295,296],{"class":54},"\"application\"",[44,298,299],{"class":240},", ",[44,301,302],{"class":54},"\"vnd.openxmlformats-officedocument.spreadsheetml.sheet\"",[44,304,305],{"class":240},")\n",[44,307,309],{"class":46,"line":308},6,[44,310,280],{"emptyLinePlaceholder":279},[44,312,314,317,321],{"class":46,"line":313},7,[44,315,316],{"class":236},"def",[44,318,320],{"class":319},"s_Opv"," build_message",[44,322,323],{"class":240},"(sender, recipients, subject, report_path):\n",[44,325,327,330,333],{"class":46,"line":326},8,[44,328,329],{"class":240},"    df ",[44,331,332],{"class":236},"=",[44,334,335],{"class":240}," pd.read_excel(report_path)\n",[44,337,339,342,344,347,350,353,356,359,362,365,368],{"class":46,"line":338},9,[44,340,341],{"class":240},"    below ",[44,343,332],{"class":236},[44,345,346],{"class":240}," df[df[",[44,348,349],{"class":54},"\"Sales\"",[44,351,352],{"class":240},"] ",[44,354,355],{"class":236},"\u003C",[44,357,358],{"class":240}," df[",[44,360,361],{"class":54},"\"Target\"",[44,363,364],{"class":240},"]][",[44,366,367],{"class":54},"\"Region\"",[44,369,370],{"class":240},"].tolist()\n",[44,372,374,377,379],{"class":46,"line":373},10,[44,375,376],{"class":240},"    msg ",[44,378,332],{"class":236},[44,380,381],{"class":240}," EmailMessage()\n",[44,383,385,388,391,393,395],{"class":46,"line":384},11,[44,386,387],{"class":240},"    msg[",[44,389,390],{"class":54},"\"Subject\"",[44,392,352],{"class":240},[44,394,332],{"class":236},[44,396,397],{"class":240}," subject\n",[44,399,401,403,406,408,410],{"class":46,"line":400},12,[44,402,387],{"class":240},[44,404,405],{"class":54},"\"From\"",[44,407,352],{"class":240},[44,409,332],{"class":236},[44,411,412],{"class":240}," sender\n",[44,414,416,418,421,423,425,428],{"class":46,"line":415},13,[44,417,387],{"class":240},[44,419,420],{"class":54},"\"To\"",[44,422,352],{"class":240},[44,424,332],{"class":236},[44,426,427],{"class":54}," \", \"",[44,429,430],{"class":240},".join(recipients)\n",[44,432,434,437,440,443,447,450,453,456,459,462,465],{"class":46,"line":433},14,[44,435,436],{"class":240},"    msg.set_content(",[44,438,439],{"class":236},"f",[44,441,442],{"class":54},"\"Weekly sales attached. Below target: ",[44,444,446],{"class":445},"sSjpA","{",[44,448,449],{"class":54},"', '",[44,451,452],{"class":240},".join(below) ",[44,454,455],{"class":236},"or",[44,457,458],{"class":54}," 'none'",[44,460,461],{"class":445},"}",[44,463,464],{"class":54},".\"",[44,466,305],{"class":240},[44,468,470],{"class":46,"line":469},15,[44,471,472],{"class":240},"    msg.add_alternative(\n",[44,474,476,479,482,484,486,488,490,492,494],{"class":46,"line":475},16,[44,477,478],{"class":236},"        f",[44,480,481],{"class":54},"\"\u003Cp>Weekly sales attached.\u003C\u002Fp>\u003Cp>\u003Cb>Below target:\u003C\u002Fb> ",[44,483,446],{"class":445},[44,485,449],{"class":54},[44,487,452],{"class":240},[44,489,455],{"class":236},[44,491,458],{"class":54},[44,493,461],{"class":445},[44,495,496],{"class":54},"\u003C\u002Fp>\"\n",[44,498,500,503,506,510,512,515,517,520,522,524,527,530,532,535],{"class":46,"line":499},17,[44,501,502],{"class":236},"        +",[44,504,505],{"class":240}," df.to_html(",[44,507,509],{"class":508},"sa561","index",[44,511,332],{"class":236},[44,513,514],{"class":286},"False",[44,516,299],{"class":240},[44,518,519],{"class":508},"border",[44,521,332],{"class":236},[44,523,109],{"class":286},[44,525,526],{"class":240},"), ",[44,528,529],{"class":508},"subtype",[44,531,332],{"class":236},[44,533,534],{"class":54},"\"html\"",[44,536,305],{"class":240},[44,538,540,543,546,548,550,553,555,558,560,562,564,566,569],{"class":46,"line":539},18,[44,541,542],{"class":240},"    msg.add_attachment(Path(report_path).read_bytes(), ",[44,544,545],{"class":508},"maintype",[44,547,332],{"class":236},[44,549,287],{"class":286},[44,551,552],{"class":240},"[",[44,554,109],{"class":286},[44,556,557],{"class":240},"], ",[44,559,529],{"class":508},[44,561,332],{"class":236},[44,563,287],{"class":286},[44,565,552],{"class":240},[44,567,568],{"class":286},"1",[44,570,571],{"class":240},"],\n",[44,573,575,578,580],{"class":46,"line":574},19,[44,576,577],{"class":508},"                       filename",[44,579,332],{"class":236},[44,581,582],{"class":240},"Path(report_path).name)\n",[44,584,586,589],{"class":46,"line":585},20,[44,587,588],{"class":236},"    return",[44,590,591],{"class":240}," msg\n",[44,593,595],{"class":46,"line":594},21,[44,596,280],{"emptyLinePlaceholder":279},[44,598,600,603,605,608,611,614,617,619,622,624,627],{"class":46,"line":599},22,[44,601,602],{"class":240},"msg ",[44,604,332],{"class":236},[44,606,607],{"class":240}," build_message(",[44,609,610],{"class":54},"\"reports@example.com\"",[44,612,613],{"class":240},", [",[44,615,616],{"class":54},"\"sales-team@example.org\"",[44,618,557],{"class":240},[44,620,621],{"class":54},"\"Weekly sales — week 39\"",[44,623,299],{"class":240},[44,625,626],{"class":54},"\"weekly_sales.xlsx\"",[44,628,305],{"class":240},[44,630,632,635,638,641,644,647],{"class":46,"line":631},23,[44,633,634],{"class":286},"print",[44,636,637],{"class":240},"(msg.get_content_type(), [part.get_content_type() ",[44,639,640],{"class":236},"for",[44,642,643],{"class":240}," part ",[44,645,646],{"class":236},"in",[44,648,649],{"class":240}," msg.iter_parts()])\n",[44,651,653,655,658,660,663,665,668,671,674,676,679],{"class":46,"line":652},24,[44,654,634],{"class":286},[44,656,657],{"class":240},"(",[44,659,439],{"class":236},[44,661,662],{"class":54},"\"",[44,664,446],{"class":445},[44,666,667],{"class":286},"len",[44,669,670],{"class":240},"(msg.as_bytes())",[44,672,673],{"class":236},":,",[44,675,461],{"class":445},[44,677,678],{"class":54}," bytes\"",[44,680,305],{"class":240},[87,682,96,688,96,691,96,694,96,699,96,704,96,715,96,720,96,725,96,727,96,734,96,739,96,743,96,748,96,755,96,760],{"viewBox":683,"role":90,"ariaLabelledBy":684,"xmlns":94,"style":687},"54 0 652 331",[685,686],"ses-mime-t","ses-mime-d","width:100%;max-width:652px;height:auto;display:block;margin:1.5rem auto;font-family:Inter,ui-sans-serif,system-ui,sans-serif",[98,689,690],{"id":685},"The structure of the message SES receives",[102,692,693],{"id":686},"Nested layers of the MIME message. The outer multipart\u002Fmixed part contains a multipart\u002Falternative part with the plain-text and HTML bodies, followed by the xlsx attachment with its spreadsheet content type and file name.",[106,695],{"x":696,"y":109,"width":697,"height":698,"fill":112},"54","652","331",[106,700],{"x":701,"y":116,"width":702,"height":703,"rx":119,"fill":133,"stroke":134,"style":122},"70.0","620.0","93.0",[705,706,708],"g",{"transform":707},"translate(368.0,29.0) scale(1.000)",[709,710],"path",{"d":711,"fill":712,"stroke":713,"style":714},"M3 6h18v12H3z M3 7l9 6 9-6","none","var(--text,#172033)","stroke-width:1.80px;stroke-linecap:round;stroke-linejoin:round",[124,716,719],{"x":137,"y":717,"style":718},"75.5","font-size:13.5px;font-weight:700;fill:var(--text,#172033);text-anchor:middle","multipart\u002Fmixed",[124,721,724],{"x":137,"y":722,"style":723},"95.0","font-size:11.5px;font-weight:400;fill:var(--text,#172033);text-anchor:middle","the whole message",[106,726],{"x":701,"y":184,"width":702,"height":703,"rx":119,"fill":120,"stroke":121,"style":122},[705,728,730],{"transform":729},"translate(368.0,132.0) scale(1.000)",[709,731],{"d":732,"fill":712,"stroke":733,"style":714},"M6 3h9l4 4v14H6z M15 3v4h4","var(--brand-strong,#4338ca)",[124,735,738],{"x":137,"y":736,"style":737},"178.5","font-size:13.5px;font-weight:700;fill:var(--brand-strong,#4338ca);text-anchor:middle","multipart\u002Falternative",[124,740,742],{"x":137,"y":741,"style":723},"198.0","text\u002Fplain summary + text\u002Fhtml summary with table",[106,744],{"x":701,"y":745,"width":702,"height":703,"rx":119,"fill":746,"stroke":747,"style":122},"222.0","#d9f4f1","var(--teal,#0f9488)",[705,749,751],{"transform":750},"translate(368.0,235.0) scale(1.000)",[709,752],{"d":753,"fill":712,"stroke":754,"style":714},"M6 3h9l4 4v14H6z M15 3v4h4 M9.5 11.5l4.5 6 M14 11.5l-4.5 6","var(--teal-ink,#0b6157)",[124,756,759],{"x":137,"y":757,"style":758},"281.5","font-size:13.5px;font-weight:700;fill:var(--teal-ink,#0b6157);text-anchor:middle","application\u002Fvnd.openxmlformats-…sheet",[124,761,763],{"x":137,"y":762,"style":723},"301.0","weekly_sales.xlsx, base64 encoded",[10,765,766,767,769,770,773,774,778],{},"The message is ",[19,768,719],{},": an alternative part holding the text and HTML versions, followed by the workbook. Mail programs show the HTML version with its table and offer the ",[19,771,772],{},".xlsx"," as an attachment; the plain-text part is what shows in clients and notification previews that do not render HTML. ",[24,775,777],{"href":776},"\u002Fautomating-reporting-workflows\u002Femailing-excel-reports-with-smtplib\u002Femail-an-excel-report-with-an-html-summary-body\u002F","Email an Excel Report with an HTML Summary Body"," covers the HTML in depth.",[30,780,782],{"id":781},"step-2-send-it-with-the-ses-v2-client","Step 2 — Send it with the SES v2 client",[10,784,785,786,789,790,793,794,797],{},"The SES v2 ",[19,787,788],{},"send_email"," call takes the raw bytes of the message in ",[19,791,792],{},"Content={\"Raw\": {\"Data\": ...}}",". Here it runs inside ",[19,795,796],{},"mock_aws()",", which is the only line that differs from production:",[35,799,801],{"className":227,"code":800,"language":229,"meta":40,"style":40},"import os\nimport boto3\nfrom moto import mock_aws\n\nos.environ.update(AWS_ACCESS_KEY_ID=\"testing\", AWS_SECRET_ACCESS_KEY=\"testing\", AWS_DEFAULT_REGION=\"eu-west-1\")\n\ndef send_report(ses, msg):\n    response = ses.send_email(\n        FromEmailAddress=msg[\"From\"],\n        Destination={\"ToAddresses\": [a.strip() for a in msg[\"To\"].split(\",\")]},\n        Content={\"Raw\": {\"Data\": msg.as_bytes()}},\n    )\n    return response[\"MessageId\"]\n\nwith mock_aws():\n    boto3.client(\"ses\").verify_email_identity(EmailAddress=\"reports@example.com\")\n    ses = boto3.client(\"sesv2\")\n    print(\"sent:\", send_report(ses, msg))\n",[19,802,803,810,817,829,833,867,871,881,891,905,941,962,967,980,984,992,1012,1027],{"__ignoreMap":40},[44,804,805,807],{"class":46,"line":47},[44,806,244],{"class":236},[44,808,809],{"class":240}," os\n",[44,811,812,814],{"class":46,"line":67},[44,813,244],{"class":236},[44,815,816],{"class":240}," boto3\n",[44,818,819,821,824,826],{"class":46,"line":262},[44,820,237],{"class":236},[44,822,823],{"class":240}," moto ",[44,825,244],{"class":236},[44,827,828],{"class":240}," mock_aws\n",[44,830,831],{"class":46,"line":276},[44,832,280],{"emptyLinePlaceholder":279},[44,834,835,838,841,843,846,848,851,853,855,857,860,862,865],{"class":46,"line":283},[44,836,837],{"class":240},"os.environ.update(",[44,839,840],{"class":508},"AWS_ACCESS_KEY_ID",[44,842,332],{"class":236},[44,844,845],{"class":54},"\"testing\"",[44,847,299],{"class":240},[44,849,850],{"class":508},"AWS_SECRET_ACCESS_KEY",[44,852,332],{"class":236},[44,854,845],{"class":54},[44,856,299],{"class":240},[44,858,859],{"class":508},"AWS_DEFAULT_REGION",[44,861,332],{"class":236},[44,863,864],{"class":54},"\"eu-west-1\"",[44,866,305],{"class":240},[44,868,869],{"class":46,"line":308},[44,870,280],{"emptyLinePlaceholder":279},[44,872,873,875,878],{"class":46,"line":313},[44,874,316],{"class":236},[44,876,877],{"class":319}," send_report",[44,879,880],{"class":240},"(ses, msg):\n",[44,882,883,886,888],{"class":46,"line":326},[44,884,885],{"class":240},"    response ",[44,887,332],{"class":236},[44,889,890],{"class":240}," ses.send_email(\n",[44,892,893,896,898,901,903],{"class":46,"line":338},[44,894,895],{"class":508},"        FromEmailAddress",[44,897,332],{"class":236},[44,899,900],{"class":240},"msg[",[44,902,405],{"class":54},[44,904,571],{"class":240},[44,906,907,910,912,914,917,920,922,925,927,930,932,935,938],{"class":46,"line":373},[44,908,909],{"class":508},"        Destination",[44,911,332],{"class":236},[44,913,446],{"class":240},[44,915,916],{"class":54},"\"ToAddresses\"",[44,918,919],{"class":240},": [a.strip() ",[44,921,640],{"class":236},[44,923,924],{"class":240}," a ",[44,926,646],{"class":236},[44,928,929],{"class":240}," msg[",[44,931,420],{"class":54},[44,933,934],{"class":240},"].split(",[44,936,937],{"class":54},"\",\"",[44,939,940],{"class":240},")]},\n",[44,942,943,946,948,950,953,956,959],{"class":46,"line":384},[44,944,945],{"class":508},"        Content",[44,947,332],{"class":236},[44,949,446],{"class":240},[44,951,952],{"class":54},"\"Raw\"",[44,954,955],{"class":240},": {",[44,957,958],{"class":54},"\"Data\"",[44,960,961],{"class":240},": msg.as_bytes()}},\n",[44,963,964],{"class":46,"line":400},[44,965,966],{"class":240},"    )\n",[44,968,969,971,974,977],{"class":46,"line":415},[44,970,588],{"class":236},[44,972,973],{"class":240}," response[",[44,975,976],{"class":54},"\"MessageId\"",[44,978,979],{"class":240},"]\n",[44,981,982],{"class":46,"line":433},[44,983,280],{"emptyLinePlaceholder":279},[44,985,986,989],{"class":46,"line":469},[44,987,988],{"class":236},"with",[44,990,991],{"class":240}," mock_aws():\n",[44,993,994,997,1000,1003,1006,1008,1010],{"class":46,"line":475},[44,995,996],{"class":240},"    boto3.client(",[44,998,999],{"class":54},"\"ses\"",[44,1001,1002],{"class":240},").verify_email_identity(",[44,1004,1005],{"class":508},"EmailAddress",[44,1007,332],{"class":236},[44,1009,610],{"class":54},[44,1011,305],{"class":240},[44,1013,1014,1017,1019,1022,1025],{"class":46,"line":499},[44,1015,1016],{"class":240},"    ses ",[44,1018,332],{"class":236},[44,1020,1021],{"class":240}," boto3.client(",[44,1023,1024],{"class":54},"\"sesv2\"",[44,1026,305],{"class":240},[44,1028,1029,1032,1034,1037],{"class":46,"line":539},[44,1030,1031],{"class":286},"    print",[44,1033,657],{"class":240},[44,1035,1036],{"class":54},"\"sent:\"",[44,1038,1039],{"class":240},", send_report(ses, msg))\n",[10,1041,1042,1043,1046,1047,1050,1051,1054,1055,1058,1059,1062,1063,1065],{},"In production there is no ",[19,1044,1045],{},"mock_aws"," and no fake credentials: boto3 finds real credentials from the environment, a profile or — on AWS — the job's IAM role, and the region should be the one where your sender identity is verified. ",[19,1048,1049],{},"Destination"," is what SES uses for delivery; the ",[19,1052,1053],{},"To"," header inside the message is only what recipients see. Keeping both from the same list, as ",[19,1056,1057],{},"send_report"," does, avoids surprises, and addresses meant to be hidden go in ",[19,1060,1061],{},"BccAddresses"," in ",[19,1064,1049],{}," only.",[30,1067,1069],{"id":1068},"step-3-handle-the-errors-ses-actually-returns","Step 3 — Handle the errors SES actually returns",[10,1071,1072,1073,1076],{},"The two errors a reporting job meets are ",[14,1074,1075],{},"MessageRejected"," — the sender is not verified, or in the sandbox the recipient is not — and throttling when the sending rate is exceeded. Rejections are configuration problems that retrying cannot fix; throttling is transient and clears on its own within seconds:",[35,1078,1080],{"className":227,"code":1079,"language":229,"meta":40,"style":40},"from botocore.config import Config\nfrom botocore.exceptions import ClientError\n\ndef send_or_explain(ses, msg):\n    try:\n        return send_report(ses, msg)\n    except ClientError as err:\n        code = err.response[\"Error\"][\"Code\"]\n        if code == \"MessageRejected\":\n            raise RuntimeError(f\"SES rejected the message: {err.response['Error']['Message']}\") from err\n        raise\n\nwith mock_aws():\n    boto3.client(\"ses\").verify_email_identity(EmailAddress=\"reports@example.com\")\n    ses = boto3.client(\"sesv2\", config=Config(retries={\"mode\": \"standard\", \"max_attempts\": 5}))\n    bad = build_message(\"nobody@unverified.example\", [\"sales-team@example.org\"], \"Test\", \"weekly_sales.xlsx\")\n    try:\n        send_or_explain(ses, bad)\n    except RuntimeError as err:\n        print(err)\n    print(boto3.client(\"ses\").get_send_quota()[\"MaxSendRate\"], \"messages per second allowed\")\n",[19,1081,1082,1094,1106,1110,1119,1127,1135,1148,1169,1185,1228,1233,1237,1243,1259,1308,1335,1341,1346,1357,1365],{"__ignoreMap":40},[44,1083,1084,1086,1089,1091],{"class":46,"line":47},[44,1085,237],{"class":236},[44,1087,1088],{"class":240}," botocore.config ",[44,1090,244],{"class":236},[44,1092,1093],{"class":240}," Config\n",[44,1095,1096,1098,1101,1103],{"class":46,"line":67},[44,1097,237],{"class":236},[44,1099,1100],{"class":240}," botocore.exceptions ",[44,1102,244],{"class":236},[44,1104,1105],{"class":240}," ClientError\n",[44,1107,1108],{"class":46,"line":262},[44,1109,280],{"emptyLinePlaceholder":279},[44,1111,1112,1114,1117],{"class":46,"line":276},[44,1113,316],{"class":236},[44,1115,1116],{"class":319}," send_or_explain",[44,1118,880],{"class":240},[44,1120,1121,1124],{"class":46,"line":283},[44,1122,1123],{"class":236},"    try",[44,1125,1126],{"class":240},":\n",[44,1128,1129,1132],{"class":46,"line":308},[44,1130,1131],{"class":236},"        return",[44,1133,1134],{"class":240}," send_report(ses, msg)\n",[44,1136,1137,1140,1143,1145],{"class":46,"line":313},[44,1138,1139],{"class":236},"    except",[44,1141,1142],{"class":240}," ClientError ",[44,1144,270],{"class":236},[44,1146,1147],{"class":240}," err:\n",[44,1149,1150,1153,1155,1158,1161,1164,1167],{"class":46,"line":326},[44,1151,1152],{"class":240},"        code ",[44,1154,332],{"class":236},[44,1156,1157],{"class":240}," err.response[",[44,1159,1160],{"class":54},"\"Error\"",[44,1162,1163],{"class":240},"][",[44,1165,1166],{"class":54},"\"Code\"",[44,1168,979],{"class":240},[44,1170,1171,1174,1177,1180,1183],{"class":46,"line":338},[44,1172,1173],{"class":236},"        if",[44,1175,1176],{"class":240}," code ",[44,1178,1179],{"class":236},"==",[44,1181,1182],{"class":54}," \"MessageRejected\"",[44,1184,1126],{"class":240},[44,1186,1187,1190,1193,1195,1197,1200,1202,1205,1208,1210,1213,1216,1218,1220,1223,1225],{"class":46,"line":373},[44,1188,1189],{"class":236},"            raise",[44,1191,1192],{"class":286}," RuntimeError",[44,1194,657],{"class":240},[44,1196,439],{"class":236},[44,1198,1199],{"class":54},"\"SES rejected the message: ",[44,1201,446],{"class":445},[44,1203,1204],{"class":240},"err.response[",[44,1206,1207],{"class":54},"'Error'",[44,1209,1163],{"class":240},[44,1211,1212],{"class":54},"'Message'",[44,1214,1215],{"class":240},"]",[44,1217,461],{"class":445},[44,1219,662],{"class":54},[44,1221,1222],{"class":240},") ",[44,1224,237],{"class":236},[44,1226,1227],{"class":240}," err\n",[44,1229,1230],{"class":46,"line":384},[44,1231,1232],{"class":236},"        raise\n",[44,1234,1235],{"class":46,"line":400},[44,1236,280],{"emptyLinePlaceholder":279},[44,1238,1239,1241],{"class":46,"line":415},[44,1240,988],{"class":236},[44,1242,991],{"class":240},[44,1244,1245,1247,1249,1251,1253,1255,1257],{"class":46,"line":433},[44,1246,996],{"class":240},[44,1248,999],{"class":54},[44,1250,1002],{"class":240},[44,1252,1005],{"class":508},[44,1254,332],{"class":236},[44,1256,610],{"class":54},[44,1258,305],{"class":240},[44,1260,1261,1263,1265,1267,1269,1271,1274,1276,1279,1282,1284,1286,1289,1292,1295,1297,1300,1302,1305],{"class":46,"line":469},[44,1262,1016],{"class":240},[44,1264,332],{"class":236},[44,1266,1021],{"class":240},[44,1268,1024],{"class":54},[44,1270,299],{"class":240},[44,1272,1273],{"class":508},"config",[44,1275,332],{"class":236},[44,1277,1278],{"class":240},"Config(",[44,1280,1281],{"class":508},"retries",[44,1283,332],{"class":236},[44,1285,446],{"class":240},[44,1287,1288],{"class":54},"\"mode\"",[44,1290,1291],{"class":240},": ",[44,1293,1294],{"class":54},"\"standard\"",[44,1296,299],{"class":240},[44,1298,1299],{"class":54},"\"max_attempts\"",[44,1301,1291],{"class":240},[44,1303,1304],{"class":286},"5",[44,1306,1307],{"class":240},"}))\n",[44,1309,1310,1313,1315,1317,1320,1322,1324,1326,1329,1331,1333],{"class":46,"line":475},[44,1311,1312],{"class":240},"    bad ",[44,1314,332],{"class":236},[44,1316,607],{"class":240},[44,1318,1319],{"class":54},"\"nobody@unverified.example\"",[44,1321,613],{"class":240},[44,1323,616],{"class":54},[44,1325,557],{"class":240},[44,1327,1328],{"class":54},"\"Test\"",[44,1330,299],{"class":240},[44,1332,626],{"class":54},[44,1334,305],{"class":240},[44,1336,1337,1339],{"class":46,"line":499},[44,1338,1123],{"class":236},[44,1340,1126],{"class":240},[44,1342,1343],{"class":46,"line":539},[44,1344,1345],{"class":240},"        send_or_explain(ses, bad)\n",[44,1347,1348,1350,1352,1355],{"class":46,"line":574},[44,1349,1139],{"class":236},[44,1351,1192],{"class":286},[44,1353,1354],{"class":236}," as",[44,1356,1147],{"class":240},[44,1358,1359,1362],{"class":46,"line":585},[44,1360,1361],{"class":286},"        print",[44,1363,1364],{"class":240},"(err)\n",[44,1366,1367,1369,1372,1374,1377,1380,1382,1385],{"class":46,"line":594},[44,1368,1031],{"class":286},[44,1370,1371],{"class":240},"(boto3.client(",[44,1373,999],{"class":54},[44,1375,1376],{"class":240},").get_send_quota()[",[44,1378,1379],{"class":54},"\"MaxSendRate\"",[44,1381,557],{"class":240},[44,1383,1384],{"class":54},"\"messages per second allowed\"",[44,1386,305],{"class":240},[10,1388,1389,1390,1393,1394,1398],{},"The ",[19,1391,1392],{},"standard"," retry mode makes boto3 retry throttling errors with exponential backoff on its own, so the script does not need its own retry loop for them. A rejection is turned into a clear message and allowed to fail the job — the right outcome, since the report never left. ",[24,1395,1397],{"href":1396},"\u002Fautomating-reporting-workflows\u002Ferror-handling-and-logging-in-excel-automation\u002Fsend-an-alert-when-an-excel-report-job-fails\u002F","Send an Alert When an Excel Report Job Fails"," covers what should happen next.",[87,1400,96,1405,96,1408,96,1411,96,1414,96,1418,96,1422,96,1425,96,1429,96,1432,96,1435,96,1438,96,1443,96,1448,96,1453,96,1459,96,1463,96,1467,96,1470,96,1473,96,1477,96,1479,96,1484,96,1491,96,1496,96,1500,96,1503,96,1506,96,1509,96,1513,96,1516,96,1521,96,1528,96,1532,96,1535,96,1537,96,1540,96,1544,96,1547,96,1550,96,1556,96,1559],{"viewBox":1401,"role":90,"ariaLabelledBy":1402,"xmlns":94,"style":95},"4 0 752 290",[1403,1404],"ses-errors-t","ses-errors-d",[98,1406,1407],{"id":1403},"What to do with each SES outcome",[102,1409,1410],{"id":1404},"A decision diagram. A message ID means the report was accepted. MessageRejected means an unverified sender or a sandbox recipient and must be fixed in configuration. Throttling is retried automatically with backoff. A message over the size limit needs a smaller attachment or a link.",[106,1412],{"x":108,"y":109,"width":110,"height":1413,"fill":112},"290",[106,1415],{"x":1416,"y":116,"width":1417,"height":717,"rx":119,"fill":120,"stroke":121,"style":122},"240.0","280.0",[705,1419,1420],{"transform":707},[709,1421],{"d":711,"fill":712,"stroke":733,"style":714},[124,1423,1424],{"x":137,"y":717,"style":737},"What did SendEmail return?",[46,1426],{"x1":137,"y1":1427,"x2":137,"y2":1428,"stroke":121,"style":122},"91.5","111.5",[46,1430],{"x1":159,"y1":1428,"x2":1431,"y2":1428,"stroke":121,"style":122},"656.0",[46,1433],{"x1":159,"y1":1428,"x2":159,"y2":1434,"stroke":121,"style":122},"157.5",[162,1436],{"points":1437,"fill":165},"104.0,161.5 99.0,152.5 109.0,152.5",[106,1439],{"x":1440,"y":1441,"width":1442,"height":171,"rx":172,"fill":112,"stroke":134,"style":173},"62.6","121.5","82.8",[124,1444,1447],{"x":159,"y":1445,"style":1446},"136.0","font-size:11.5px;font-weight:700;fill:var(--muted,#5b6780);text-anchor:middle","MessageId",[106,1449],{"x":115,"y":1450,"width":1451,"height":1452,"rx":119,"fill":746,"stroke":747,"style":122},"163.5","168.0","110.5",[705,1454,1456],{"transform":1455},"translate(92.0,185.2) scale(1.000)",[709,1457],{"d":1458,"fill":712,"stroke":754,"style":714},"M5 12.5l4.5 4.5L19 7",[124,1460,1462],{"x":159,"y":1461,"style":758},"231.8","Accepted",[124,1464,1466],{"x":159,"y":1465,"style":723},"251.2","log the ID",[46,1468],{"x1":1469,"y1":1428,"x2":1469,"y2":1434,"stroke":121,"style":122},"288.0",[162,1471],{"points":1472,"fill":165},"288.0,161.5 283.0,152.5 293.0,152.5",[106,1474],{"x":1475,"y":1441,"width":1476,"height":171,"rx":172,"fill":112,"stroke":134,"style":173},"226.1","123.7",[124,1478,1075],{"x":1469,"y":1445,"style":1446},[106,1480],{"x":1481,"y":1450,"width":1451,"height":1452,"rx":119,"fill":1482,"stroke":1483,"style":122},"204.0","#fce9e9","var(--danger,#dc2626)",[705,1485,1487],{"transform":1486},"translate(276.0,176.5) scale(1.000)",[709,1488],{"d":1489,"fill":712,"stroke":1490,"style":714},"M12 3.5l9.5 17h-19z M12 10v5 M12 17.8v.4","var(--accent-ink,#be185d)",[124,1492,1495],{"x":1469,"y":1493,"style":1494},"223.0","font-size:13.5px;font-weight:700;fill:var(--accent-ink,#be185d);text-anchor:middle","Fix configuration",[124,1497,1499],{"x":1469,"y":1498,"style":723},"242.5","verify sender, leave",[124,1501,84],{"x":1469,"y":1502,"style":723},"260.0",[46,1504],{"x1":1505,"y1":1428,"x2":1505,"y2":1434,"stroke":121,"style":122},"472.0",[162,1507],{"points":1508,"fill":165},"472.0,161.5 467.0,152.5 477.0,152.5",[106,1510],{"x":1511,"y":1441,"width":1512,"height":171,"rx":172,"fill":112,"stroke":134,"style":173},"433.8","76.4",[124,1514,1515],{"x":1505,"y":1445,"style":1446},"Throttling",[106,1517],{"x":1518,"y":1450,"width":1451,"height":1452,"rx":119,"fill":1519,"stroke":1520,"style":122},"388.0","#fdefd8","var(--gold,#b4740a)",[705,1522,1524],{"transform":1523},"translate(460.0,185.2) scale(1.000)",[709,1525],{"d":1526,"fill":712,"stroke":1527,"style":714},"M20 11a8 8 0 1 0-2.3 5.7 M20 4v7h-7","var(--gold-ink,#7a4e06)",[124,1529,1531],{"x":1505,"y":1461,"style":1530},"font-size:13.5px;font-weight:700;fill:var(--gold-ink,#7a4e06);text-anchor:middle","Retry with backoff",[124,1533,1534],{"x":1505,"y":1465,"style":723},"botocore standard mode",[46,1536],{"x1":1431,"y1":1428,"x2":1431,"y2":1434,"stroke":121,"style":122},[162,1538],{"points":1539,"fill":165},"656.0,161.5 651.0,152.5 661.0,152.5",[106,1541],{"x":1542,"y":1441,"width":1543,"height":171,"rx":172,"fill":112,"stroke":134,"style":173},"620.9","70.1",[124,1545,1546],{"x":1431,"y":1445,"style":1446},"too large",[106,1548],{"x":1549,"y":1450,"width":1451,"height":1452,"rx":119,"fill":120,"stroke":121,"style":122},"572.0",[705,1551,1553],{"transform":1552},"translate(644.0,185.2) scale(1.000)",[709,1554],{"d":1555,"fill":712,"stroke":733,"style":714},"M12 2.5l8.5 4.7v9.6L12 21.5l-8.5-4.7V7.2z M3.5 7.2l8.5 4.7 8.5-4.7 M12 11.9v9.6",[124,1557,1558],{"x":1431,"y":1461,"style":737},"Shrink or link",[124,1560,1561],{"x":1431,"y":1465,"style":723},"zip, or S3 link",[30,1563,1565],{"id":1564},"step-4-stay-under-the-size-limit","Step 4 — Stay under the size limit",[10,1567,1568],{},"SES limits the size of a message, including attachments after base64 encoding, which inflates binary data by about a third. Check the encoded size before sending, and fall back to a link when the workbook is too big:",[35,1570,1572],{"className":227,"code":1571,"language":229,"meta":40,"style":40},"MAX_BYTES = 10 * 1024 * 1024          # stay well under the service limit and recipients' mailbox limits\n\nsize = len(msg.as_bytes())\nprint(f\"message is {size \u002F 1024:.1f} KB of {MAX_BYTES \u002F 1024 \u002F 1024:.0f} MB budget\")\nif size > MAX_BYTES:\n    print(\"too large: upload the workbook and send a link instead\")\n",[19,1573,1574,1597,1601,1614,1665,1681],{"__ignoreMap":40},[44,1575,1576,1579,1581,1584,1587,1590,1592,1594],{"class":46,"line":47},[44,1577,1578],{"class":286},"MAX_BYTES",[44,1580,290],{"class":236},[44,1582,1583],{"class":286}," 10",[44,1585,1586],{"class":236}," *",[44,1588,1589],{"class":286}," 1024",[44,1591,1586],{"class":236},[44,1593,1589],{"class":286},[44,1595,1596],{"class":77},"          # stay well under the service limit and recipients' mailbox limits\n",[44,1598,1599],{"class":46,"line":67},[44,1600,280],{"emptyLinePlaceholder":279},[44,1602,1603,1606,1608,1611],{"class":46,"line":262},[44,1604,1605],{"class":240},"size ",[44,1607,332],{"class":236},[44,1609,1610],{"class":286}," len",[44,1612,1613],{"class":240},"(msg.as_bytes())\n",[44,1615,1616,1618,1620,1622,1625,1627,1629,1632,1634,1637,1639,1642,1644,1646,1649,1651,1653,1655,1658,1660,1663],{"class":46,"line":276},[44,1617,634],{"class":286},[44,1619,657],{"class":240},[44,1621,439],{"class":236},[44,1623,1624],{"class":54},"\"message is ",[44,1626,446],{"class":445},[44,1628,1605],{"class":240},[44,1630,1631],{"class":236},"\u002F",[44,1633,1589],{"class":286},[44,1635,1636],{"class":236},":.1f",[44,1638,461],{"class":445},[44,1640,1641],{"class":54}," KB of ",[44,1643,446],{"class":445},[44,1645,1578],{"class":286},[44,1647,1648],{"class":236}," \u002F",[44,1650,1589],{"class":286},[44,1652,1648],{"class":236},[44,1654,1589],{"class":286},[44,1656,1657],{"class":236},":.0f",[44,1659,461],{"class":445},[44,1661,1662],{"class":54}," MB budget\"",[44,1664,305],{"class":240},[44,1666,1667,1670,1673,1676,1679],{"class":46,"line":283},[44,1668,1669],{"class":236},"if",[44,1671,1672],{"class":240}," size ",[44,1674,1675],{"class":236},">",[44,1677,1678],{"class":286}," MAX_BYTES",[44,1680,1126],{"class":240},[44,1682,1683,1685,1687,1690],{"class":46,"line":308},[44,1684,1031],{"class":286},[44,1686,657],{"class":240},[44,1688,1689],{"class":54},"\"too large: upload the workbook and send a link instead\"",[44,1691,305],{"class":240},[10,1693,1694,1695,1699,1700,1704],{},"A budget of 10 MB is deliberately below SES's own limit, because many recipients' mail servers reject large messages long before SES would. For bigger reports, compress them first — ",[24,1696,1698],{"href":1697},"\u002Fautomating-reporting-workflows\u002Femailing-excel-reports-with-smtplib\u002Fzip-large-excel-attachments-before-emailing\u002F","Zip Large Excel Attachments Before Emailing"," — or upload them to S3 and send a presigned link, as in ",[24,1701,1703],{"href":1702},"\u002Fautomating-reporting-workflows\u002Fpublishing-excel-reports-to-cloud-storage\u002Fupload-an-excel-report-to-amazon-s3-with-boto3\u002F","Upload an Excel Report to Amazon S3 with boto3",".",[30,1706,1708],{"id":1707},"step-5-test-the-message-end-to-end-with-moto","Step 5 — Test the message end to end with moto",[10,1710,1711],{},"moto records every message SES \"sends\", so a test can open the raw bytes, parse them back into an email, and check the recipients and the attachment — the parts that break silently:",[35,1713,1715],{"className":227,"code":1714,"language":229,"meta":40,"style":40},"import io\nfrom email import message_from_bytes, policy\nfrom moto.core import DEFAULT_ACCOUNT_ID\nfrom moto.ses.models import ses_backends\n\nwith mock_aws():\n    boto3.client(\"ses\").verify_email_identity(EmailAddress=\"reports@example.com\")\n    send_report(boto3.client(\"sesv2\"), msg)\n    sent = ses_backends[DEFAULT_ACCOUNT_ID][\"eu-west-1\"].sent_messages\n    raw = sent[0].raw_data\n    parsed = message_from_bytes(raw.encode() if isinstance(raw, str) else raw, policy=policy.default)\n    attachment = next(parsed.iter_attachments())\n    body = pd.read_excel(io.BytesIO(attachment.get_content()))\n    print(len(sent), \"sent to\", sorted(set(sent[0].destinations)))\n    print(attachment.get_filename(), attachment.get_content_type())\n    print(body.shape, \"regions:\", body[\"Region\"].tolist())\n",[19,1716,1717,1724,1736,1748,1760,1764,1770,1786,1796,1816,1831,1868,1881,1891,1923,1930],{"__ignoreMap":40},[44,1718,1719,1721],{"class":46,"line":47},[44,1720,244],{"class":236},[44,1722,1723],{"class":240}," io\n",[44,1725,1726,1728,1731,1733],{"class":46,"line":67},[44,1727,237],{"class":236},[44,1729,1730],{"class":240}," email ",[44,1732,244],{"class":236},[44,1734,1735],{"class":240}," message_from_bytes, policy\n",[44,1737,1738,1740,1743,1745],{"class":46,"line":262},[44,1739,237],{"class":236},[44,1741,1742],{"class":240}," moto.core ",[44,1744,244],{"class":236},[44,1746,1747],{"class":286}," DEFAULT_ACCOUNT_ID\n",[44,1749,1750,1752,1755,1757],{"class":46,"line":276},[44,1751,237],{"class":236},[44,1753,1754],{"class":240}," moto.ses.models ",[44,1756,244],{"class":236},[44,1758,1759],{"class":240}," ses_backends\n",[44,1761,1762],{"class":46,"line":283},[44,1763,280],{"emptyLinePlaceholder":279},[44,1765,1766,1768],{"class":46,"line":308},[44,1767,988],{"class":236},[44,1769,991],{"class":240},[44,1771,1772,1774,1776,1778,1780,1782,1784],{"class":46,"line":313},[44,1773,996],{"class":240},[44,1775,999],{"class":54},[44,1777,1002],{"class":240},[44,1779,1005],{"class":508},[44,1781,332],{"class":236},[44,1783,610],{"class":54},[44,1785,305],{"class":240},[44,1787,1788,1791,1793],{"class":46,"line":326},[44,1789,1790],{"class":240},"    send_report(boto3.client(",[44,1792,1024],{"class":54},[44,1794,1795],{"class":240},"), msg)\n",[44,1797,1798,1801,1803,1806,1809,1811,1813],{"class":46,"line":338},[44,1799,1800],{"class":240},"    sent ",[44,1802,332],{"class":236},[44,1804,1805],{"class":240}," ses_backends[",[44,1807,1808],{"class":286},"DEFAULT_ACCOUNT_ID",[44,1810,1163],{"class":240},[44,1812,864],{"class":54},[44,1814,1815],{"class":240},"].sent_messages\n",[44,1817,1818,1821,1823,1826,1828],{"class":46,"line":373},[44,1819,1820],{"class":240},"    raw ",[44,1822,332],{"class":236},[44,1824,1825],{"class":240}," sent[",[44,1827,109],{"class":286},[44,1829,1830],{"class":240},"].raw_data\n",[44,1832,1833,1836,1838,1841,1843,1846,1849,1852,1854,1857,1860,1863,1865],{"class":46,"line":384},[44,1834,1835],{"class":240},"    parsed ",[44,1837,332],{"class":236},[44,1839,1840],{"class":240}," message_from_bytes(raw.encode() ",[44,1842,1669],{"class":236},[44,1844,1845],{"class":286}," isinstance",[44,1847,1848],{"class":240},"(raw, ",[44,1850,1851],{"class":286},"str",[44,1853,1222],{"class":240},[44,1855,1856],{"class":236},"else",[44,1858,1859],{"class":240}," raw, ",[44,1861,1862],{"class":508},"policy",[44,1864,332],{"class":236},[44,1866,1867],{"class":240},"policy.default)\n",[44,1869,1870,1873,1875,1878],{"class":46,"line":400},[44,1871,1872],{"class":240},"    attachment ",[44,1874,332],{"class":236},[44,1876,1877],{"class":286}," next",[44,1879,1880],{"class":240},"(parsed.iter_attachments())\n",[44,1882,1883,1886,1888],{"class":46,"line":415},[44,1884,1885],{"class":240},"    body ",[44,1887,332],{"class":236},[44,1889,1890],{"class":240}," pd.read_excel(io.BytesIO(attachment.get_content()))\n",[44,1892,1893,1895,1897,1899,1902,1905,1907,1910,1912,1915,1918,1920],{"class":46,"line":433},[44,1894,1031],{"class":286},[44,1896,657],{"class":240},[44,1898,667],{"class":286},[44,1900,1901],{"class":240},"(sent), ",[44,1903,1904],{"class":54},"\"sent to\"",[44,1906,299],{"class":240},[44,1908,1909],{"class":286},"sorted",[44,1911,657],{"class":240},[44,1913,1914],{"class":286},"set",[44,1916,1917],{"class":240},"(sent[",[44,1919,109],{"class":286},[44,1921,1922],{"class":240},"].destinations)))\n",[44,1924,1925,1927],{"class":46,"line":469},[44,1926,1031],{"class":286},[44,1928,1929],{"class":240},"(attachment.get_filename(), attachment.get_content_type())\n",[44,1931,1932,1934,1937,1940,1943,1945],{"class":46,"line":475},[44,1933,1031],{"class":286},[44,1935,1936],{"class":240},"(body.shape, ",[44,1938,1939],{"class":54},"\"regions:\"",[44,1941,1942],{"class":240},", body[",[44,1944,367],{"class":54},[44,1946,1947],{"class":240},"].tolist())\n",[10,1949,1950,1951,1953,1954,1956,1957,1960,1961,1965],{},"moto records the recipient once from ",[19,1952,1049],{}," and once from the message's ",[19,1955,1053],{}," header, which is why the test compares a set of addresses rather than the raw list. The attachment itself comes back as the same workbook — pandas reads the four regions from it — with the right file name and content type. A test like this in the project's test suite catches the classic mistakes: attaching the previous run's file, a wrong MIME type that makes Outlook show the report as ",[19,1958,1959],{},"ATT00001.bin",", or a recipient list that lost an address. ",[24,1962,1964],{"href":1963},"\u002Fautomating-reporting-workflows\u002Femailing-excel-reports-with-smtplib\u002Ftest-report-emails-with-a-local-smtp-server\u002F","Test Report Emails with a Local SMTP Server"," does the same for SMTP.",[30,1967,1969],{"id":1968},"common-pitfalls","Common pitfalls",[1971,1972,1973,1983,1989,1995,2008],"ul",{},[1974,1975,1976,1980,1981,1704],"li",{},[1977,1978,1979],"strong",{},"Using the simple API for attachments."," Attachments need the raw message format; build it with ",[19,1982,223],{},[1974,1984,1985,1988],{},[1977,1986,1987],{},"Sending from an unverified identity."," SES rejects it; verify the domain (preferably) or the address in the same region.",[1974,1990,1991,1994],{},[1977,1992,1993],{},"Forgetting the sandbox."," New accounts can only send to verified recipients until production access is granted.",[1974,1996,1997,2000,2001,2004,2005,2007],{},[1977,1998,1999],{},"A generic content type."," ",[19,2002,2003],{},"application\u002Foctet-stream"," works but some clients show a meaningless name; use the ",[19,2006,772],{}," MIME type.",[1974,2009,2010,2013],{},[1977,2011,2012],{},"Retrying rejections."," Only throttling is worth retrying; rejections need a configuration fix.",[30,2015,2017],{"id":2016},"conclusion","Conclusion",[10,2019,2020,2021,2023,2024,2026],{},"Build the email with ",[19,2022,223],{}," — text, an HTML alternative and the workbook attached with its proper MIME type — and send its bytes with the SES v2 ",[19,2025,788],{}," call using raw content. Verify the sending domain, leave the sandbox before emailing real recipients, let boto3's standard retry mode absorb throttling, check the encoded size against a conservative budget, and test the whole flow with moto by parsing the sent message and reading the attachment back.",[30,2028,2030],{"id":2029},"frequently-asked-questions","Frequently asked questions",[10,2032,2033,2036],{},[1977,2034,2035],{},"How do I send an email with an attachment using Amazon SES in Python?","\nBuild a MIME message with email.message.EmailMessage, add the file with add_attachment, and pass msg.as_bytes() to the SES v2 client's send_email in Content={\"Raw\": {\"Data\": ...}}. SES delivers the message as built.",[10,2038,2039,2042],{},[1977,2040,2041],{},"Why does SES reject my message?","\nThe most common causes are a sender address or domain that is not verified in the region you are sending from, and — for accounts still in the sandbox — a recipient that is not verified. The error message says which.",[10,2044,2045,2048],{},[1977,2046,2047],{},"How large can an SES email attachment be?","\nSES limits the total message size, including attachments after base64 encoding, which adds about a third to binary files. Many recipients' servers accept less, so keep report emails small and send a link for large workbooks.",[10,2050,2051,2054],{},[1977,2052,2053],{},"How can I test SES emails without sending real mail?","\nUse moto's mock_aws: it imitates SES in memory and records sent messages, whose raw data you can parse back into an email to check recipients and attachments.",[30,2056,2058],{"id":2057},"related","Related",[1971,2060,2061,2067,2074,2081],{},[1974,2062,2063,2064,2066],{},"Up one level: ",[24,2065,27],{"href":26}," — every way to deliver reports by email.",[1974,2068,2069,2073],{},[24,2070,2072],{"href":2071},"\u002Fautomating-reporting-workflows\u002Femailing-excel-reports-with-smtplib\u002Fsend-an-excel-report-with-microsoft-graph-api\u002F","Send an Excel Report with the Microsoft Graph API"," — the Microsoft 365 equivalent.",[1974,2075,2076,2080],{},[24,2077,2079],{"href":2078},"\u002Fautomating-reporting-workflows\u002Fscheduling-python-excel-scripts-with-cron\u002Frun-an-excel-report-in-aws-lambda\u002F","Run an Excel Report in AWS Lambda"," — where SES sending usually runs.",[1974,2082,2083,2087],{},[24,2084,2086],{"href":2085},"\u002Fautomating-reporting-workflows\u002Ftesting-and-packaging-excel-automation-scripts\u002Fstore-credentials-for-excel-report-scripts-safely\u002F","Store Credentials for Excel Report Scripts Safely"," — why IAM roles beat stored keys.",[2089,2090,2091],"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 pre.shiki code .s-wDw, html code.shiki .s-wDw{--shiki-default:#6A737D;--shiki-dark:#BDC4CC}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .s-kum, html code.shiki .s-kum{--shiki-default:#D73A49;--shiki-dark:#FF9492}html pre.shiki code .skGVy, html code.shiki .skGVy{--shiki-default:#24292E;--shiki-dark:#F0F3F6}html pre.shiki code .sP0c6, html code.shiki .sP0c6{--shiki-default:#005CC5;--shiki-dark:#91CBFF}html pre.shiki code .s_Opv, html code.shiki .s_Opv{--shiki-default:#6F42C1;--shiki-dark:#DBB7FF}html pre.shiki code .sSjpA, html code.shiki .sSjpA{--shiki-default:#005CC5;--shiki-dark:#FF9492}html pre.shiki code .sa561, html code.shiki .sa561{--shiki-default:#E36209;--shiki-dark:#FFB757}",{"title":40,"searchDepth":67,"depth":67,"links":2093},[2094,2095,2096,2097,2098,2099,2100,2101,2102,2103],{"id":32,"depth":67,"text":33},{"id":217,"depth":67,"text":218},{"id":781,"depth":67,"text":782},{"id":1068,"depth":67,"text":1069},{"id":1564,"depth":67,"text":1565},{"id":1707,"depth":67,"text":1708},{"id":1968,"depth":67,"text":1969},{"id":2016,"depth":67,"text":2017},{"id":2029,"depth":67,"text":2030},{"id":2057,"depth":67,"text":2058},"2026-09-29","Email a generated workbook through Amazon Simple Email Service from Python: build the MIME message with an HTML summary and the .xlsx attached, send it with boto3's SES v2 client, handle unverified senders, the sandbox and throttling, stay under the size limit, and test the whole flow locally with moto — no AWS account or real email needed.","md",[2108,2110,2112,2114],{"q":2035,"a":2109},"Build a MIME message with email.message.EmailMessage, add the file with add_attachment, and pass msg.as_bytes() to the SES v2 client's send_email in Content={\"Raw\": {\"Data\": ...}}. SES delivers the message as built.",{"q":2041,"a":2111},"The most common causes are a sender address or domain that is not verified in the region you are sending from, and — for accounts still in the sandbox — a recipient that is not verified. The error message says which.",{"q":2047,"a":2113},"SES limits the total message size, including attachments after base64 encoding, which adds about a third to binary files. Many recipients' servers accept less, so keep report emails small and send a link for large workbooks.",{"q":2053,"a":2115},"Use moto's mock_aws: it imitates SES in memory and records sent messages, whose raw data you can parse back into an email to check recipients and attachments.","send an excel report with amazon ses",null,{"breadcrumb":2119},[2120,2122,2125],{"name":2121,"item":1631},"Home",{"name":2123,"item":2124},"Automating Reporting Workflows with Python and Excel","\u002Fautomating-reporting-workflows\u002F",{"name":27,"item":26},"\u002Fautomating-reporting-workflows\u002Femailing-excel-reports-with-smtplib\u002Fsend-an-excel-report-with-amazon-ses",{"title":2128,"description":2129},"Send Excel Attachments with Amazon SES and Python boto3","Send an Excel report by email with Amazon SES v2 and boto3: EmailMessage with an .xlsx attachment, send_email with Raw content, verified identities and sandbox limits, MessageRejected and throttling retries, size checks, and local tests with moto.","send-an-excel-report-with-amazon-ses","automating-reporting-workflows\u002Femailing-excel-reports-with-smtplib\u002Fsend-an-excel-report-with-amazon-ses\u002Findex",[2133,2134],"email-delivery","publishing","how-to","QlyF03u2Vo6-J6X3PfF6eGKGq78VNj25p2Mhoc23sGw",[2138,2142],{"title":2139,"path":2140,"stem":2141,"children":-1},"Send an Excel Report Through Outlook from Python","\u002Fautomating-reporting-workflows\u002Femailing-excel-reports-with-smtplib\u002Fsend-an-excel-report-through-outlook-from-python","automating-reporting-workflows\u002Femailing-excel-reports-with-smtplib\u002Fsend-an-excel-report-through-outlook-from-python\u002Findex",{"title":2143,"path":2144,"stem":2145,"children":-1},"Send an Excel Report with Gmail from Python","\u002Fautomating-reporting-workflows\u002Femailing-excel-reports-with-smtplib\u002Fsend-an-excel-report-with-gmail","automating-reporting-workflows\u002Femailing-excel-reports-with-smtplib\u002Fsend-an-excel-report-with-gmail\u002Findex",[2147,2149],{"path":2148,"title":27},"\u002Fautomating-reporting-workflows\u002Femailing-excel-reports-with-smtplib",{"path":2150,"title":2123},"\u002Fautomating-reporting-workflows",[],[2153,2156,2159,2162,2165,2167],{"path":2154,"title":2155,"kind":2135},"\u002Fautomating-reporting-workflows\u002Fbuilding-finance-and-operations-reports\u002Fbuild-a-credit-control-dunning-letter-run","Build a Credit Control Dunning Letter Run",{"path":2157,"title":2158,"kind":2135},"\u002Fautomating-reporting-workflows\u002Femailing-excel-reports-with-smtplib\u002Fattach-multiple-excel-files-to-one-email-in-python","Attach Multiple Excel Files to One Email in Python",{"path":2160,"title":2161,"kind":2135},"\u002Fautomating-reporting-workflows\u002Femailing-excel-reports-with-smtplib\u002Fdownload-excel-attachments-from-an-email-inbox","Download Excel Attachments from an Email Inbox with Python",{"path":2163,"title":2164,"kind":2135},"\u002Fautomating-reporting-workflows\u002Femailing-excel-reports-with-smtplib\u002Femail-a-download-link-instead-of-an-excel-attachment","Email a Download Link Instead of an Excel Attachment",{"path":2166,"title":777,"kind":2135},"\u002Fautomating-reporting-workflows\u002Femailing-excel-reports-with-smtplib\u002Femail-an-excel-report-with-an-html-summary-body",{"path":2168,"title":2169,"kind":2135},"\u002Fautomating-reporting-workflows\u002Femailing-excel-reports-with-smtplib\u002Fembed-a-chart-image-in-the-body-of-a-report-email","Embed a Chart Image in the Body of a Report Email",1790972129793]