Python icalendar发送Outlook邀请仅首个收件人收到,如何修复?
问题
我有一个定期运行的Python脚本,用于向所有收件人发送邮件邀请,告知即将到来的维护工作。测试时发现,仅receivers列表中的第一个收件人能收到Outlook邀请,其余收件人无法正常收到。以下是当前代码:
import os import uuid import smtplib import icalendar import datetime from email.mime.base import MIMEBase from email.mime.text import MIMEText from email.mime.multipart import MIMEMultipart from email import encoders import pytz from jinja2 import FileSystemLoader, Environment class EmailWriter: SMTP = 'smtp.test.com' def __init__(self, receivers, cluster_name, dtstart=None, dtend=None, available="", tasks=""): self.sender = 'sender@test.com' self.smtp = smtplib.SMTP(EmailWriter.SMTP) self.receivers = receivers self.cluster_name = cluster_name self.dtstart = dtstart self.dtend = dtend self.available = available self.tasks = tasks def __get_email_subject_and_content(self): path = os.path.join(os.getcwd(), 'email_templates') loader = FileSystemLoader(path) env = Environment(loader=loader) template_minor = env.get_template('minor_maintenance_email.html') template_major = env.get_template('major_maintenance_email.html') if 'unavailability' in self.available.lower(): html_content = template_major.render( availability=self.available, maintenance_date=self.dtstart, start_time=self.dtstart, expected_end_time=self.dtend, tasks=self.tasks ) subject = '{} | Maintenance | {}'.format(self.cluster_name, self.available) else: html_content = template_minor.render() subject = '{} | Maintenance | 100% Availability'.format(self.cluster_name) print('subject : "{}", receivers : "{}", maintenance_date : "{}", start_time : "{}", expected_end_time : "{}", ' '"task : "{}"'.format(subject, self.receivers, self.dtstart, self.dtstart, self.dtend, self.tasks)) return subject, html_content def __prepare_event(self, subject, content, start, end): event = icalendar.Event() organizer = icalendar.vCalAddress('MAILTO:' + self.sender) event.add('organizer', organizer) event.add('status', 'confirmed') event.add('category', 'Event') event.add('summary', subject) event.add('description', content) event.add('dtstart', start) event.add('dtend', end) event.add('dtstamp', datetime.datetime.now()) event['uid'] = uuid.uuid4() # Set the busy status of the appointment to free event.add('X-MICROSOFT-CDO-BUSYSTATUS', icalendar.vText('FREE')) event.add('priority', 5) event.add('sequence', 0) event.add('created', datetime.datetime.now()) for participant in self.receivers: attendee = icalendar.vCalAddress('MAILTO:' + participant) attendee.params['ROLE'] = icalendar.vText('REQ-PARTICIPANT') attendee.params['cn'] = icalendar.vText(' '.join(participant.split('@')[0].split('.'))) event.add('attendee', attendee, encode=0) return event def __prepare_alarm(self): alarm = icalendar.Alarm() alarm.add('action', 'DISPLAY') alarm.add('description', 'Reminder') # The only way to convince Outlook to do it correctly alarm.add('TRIGGER;RELATED=START', '-PT{0}H'.format(1)) return alarm def __prepare_icalendar(self): # Build the event itself cal = icalendar.Calendar() cal.add('prodid', icalendar.vText('-//Calendar Application//')) cal.add('version', icalendar.vInt(2.0)) cal.add('method', icalendar.vText('REQUEST')) # creates one instance of the event cal.add('X-MS-OLK-FORCEINSPECTOROPEN', icalendar.vBoolean(True)) return cal def __prepare_email_message(self, subject, content): # Build the email message # msg = MIMEMultipart('alternative') msg = MIMEMultipart('mixed') msg['Subject'] = subject msg['From'] = self.sender msg['To'] = ';'.join(self.receivers) msg['Content-class'] = 'urn:content-classes:calendarmessage' msg.attach(MIMEText(content, 'html', 'utf-8')) return msg def __prepare_invite_blocker(self, cal): filename = 'invite.ics' part = MIMEBase('text', 'calendar', method='REQUEST', name=filename) part.set_payload(cal.to_ical()) encoders.encode_base64(part) part.add_header('Content-Description', filename) part.add_header('Filename', filename) part.add_header('Path', filename) return part def send_appointment(self): subject, html_content = self.__get_email_subject_and_content() start = datetime.datetime.combine(self.dtstart, datetime.time(0, 0, 0)).astimezone(pytz.UTC) end = datetime.datetime.combine(self.dtend, datetime.time(0, 0, 0)).astimezone(pytz.UTC) cal = self.__prepare_icalendar() event = self.__prepare_event(subject, html_content, start, end) alarm = self.__prepare_alarm() # Add a reminder event.add_component(alarm) cal.add_component(event) part = self.__prepare_invite_blocker(cal) msg = self.__prepare_email_message(subject, html_content) msg.attach(part) # Send the email out self.smtp.sendmail(msg["From"], [msg["To"]], msg.as_string()) self.smtp.quit() print('Invitation sent out') def main(): receivers = ['test1@test.com', 'test2@test.com', 'test3@test.com'] cluster_name = 'TEST NOW (test_now)' # test cluster name email_writer = EmailWriter( receivers, cluster_name, datetime.datetime.strptime('2023-02-16', '%Y-%m-%d').date(), datetime.datetime.strptime('2023-02-16', '%Y-%m-%d').date() + datetime.timedelta(days=1), '100% Availability', tasks='Minor test' ) print('Sending email') email_writer.send_appointment() if __name__ == '__main__': main()
解决方案
问题核心在于SMTP发送时的收件人参数处理错误,同时邮件头格式可以更规范,以下是两处修改:
1. 修复SMTP发送的收件人列表
原代码中sendmail方法的第二个参数传入了[msg["To"]],这会把拼接后的分号分隔字符串当成单个收件人,导致SMTP服务器只识别第一个地址。需要直接传入原始的self.receivers列表:
# 原代码 # self.smtp.sendmail(msg["From"], [msg["To"]], msg.as_string()) # 修改后 self.smtp.sendmail(msg["From"], self.receivers, msg.as_string())
2. 规范邮件头To字段格式(可选但推荐)
虽然Outlook支持分号分隔收件人,但标准邮件协议RFC推荐使用逗号分隔,修改__prepare_email_message中的To字段拼接方式:
# 原代码 # msg['To'] = ';'.join(self.receivers) # 修改后 msg['To'] = ','.join(self.receivers)
修改后的完整代码
import os import uuid import smtplib import icalendar import datetime from email.mime.base import MIMEBase from email.mime.text import MIMEText from email.mime.multipart import MIMEMultipart from email import encoders import pytz from jinja2 import FileSystemLoader, Environment class EmailWriter: SMTP = 'smtp.test.com' def __init__(self, receivers, cluster_name, dtstart=None, dtend=None, available="", tasks=""): self.sender = 'sender@test.com' self.smtp = smtplib.SMTP(EmailWriter.SMTP) self.receivers = receivers self.cluster_name = cluster_name self.dtstart = dtstart self.dtend = dtend self.available = available self.tasks = tasks def __get_email_subject_and_content(self): path = os.path.join(os.getcwd(), 'email_templates') loader = FileSystemLoader(path) env = Environment(loader=loader) template_minor = env.get_template('minor_maintenance_email.html') template_major = env.get_template('major_maintenance_email.html') if 'unavailability' in self.available.lower(): html_content = template_major.render( availability=self.available, maintenance_date=self.dtstart, start_time=self.dtstart, expected_end_time=self.dtend, tasks=self.tasks ) subject = '{} | Maintenance | {}'.format(self.cluster_name, self.available) else: html_content = template_minor.render() subject = '{} | Maintenance | 100% Availability'.format(self.cluster_name) print('subject : "{}", receivers : "{}", maintenance_date : "{}", start_time : "{}", expected_end_time : "{}", ' '"task : "{}"'.format(subject, self.receivers, self.dtstart, self.dtstart, self.dtend, self.tasks)) return subject, html_content def __prepare_event(self, subject, content, start, end): event = icalendar.Event() organizer = icalendar.vCalAddress('MAILTO:' + self.sender) event.add('organizer', organizer) event.add('status', 'confirmed') event.add('category', 'Event') event.add('summary', subject) event.add('description', content) event.add('dtstart', start) event.add('dtend', end) event.add('dtstamp', datetime.datetime.now()) event['uid'] = uuid.uuid4() # Set the busy status of the appointment to free event.add('X-MICROSOFT-CDO-BUSYSTATUS', icalendar.vText('FREE')) event.add('priority', 5) event.add('sequence', 0) event.add('created', datetime.datetime.now()) for participant in self.receivers: attendee = icalendar.vCalAddress('MAILTO:' + participant) attendee.params['ROLE'] = icalendar.vText('REQ-PARTICIPANT') attendee.params['cn'] = icalendar.vText(' '.join(participant.split('@')[0].split('.'))) event.add('attendee', attendee, encode=0) return event def __prepare_alarm(self): alarm = icalendar.Alarm() alarm.add('action', 'DISPLAY') alarm.add('description', 'Reminder') # The only way to convince Outlook to do it correctly alarm.add('TRIGGER;RELATED=START', '-PT{0}H'.format(1)) return alarm def __prepare_icalendar(self): # Build the event itself cal = icalendar.Calendar() cal.add('prodid', icalendar.vText('-//Calendar Application//')) cal.add('version', icalendar.vInt(2.0)) cal.add('method', icalendar.vText('REQUEST')) # creates one instance of the event cal.add('X-MS-OLK-FORCEINSPECTOROPEN', icalendar.vBoolean(True)) return cal def __prepare_email_message(self, subject, content): # Build the email message # msg = MIMEMultipart('alternative') msg = MIMEMultipart('mixed') msg['Subject'] = subject msg['From'] = self.sender # 用逗号分隔收件人,符合标准邮件协议 msg['To'] = ','.join(self.receivers) msg['Content-class'] = 'urn:content-classes:calendarmessage' msg.attach(MIMEText(content, 'html', 'utf-8')) return msg def __prepare_invite_blocker(self, cal): filename = 'invite.ics' part = MIMEBase('text', 'calendar', method='REQUEST', name=filename) part.set_payload(cal.to_ical()) encoders.encode_base64(part) part.add_header('Content-Description', filename) part.add_header('Filename', filename) part.add_header('Path', filename) return part def send_appointment(self): subject, html_content = self.__get_email_subject_and_content() start = datetime.datetime.combine(self.dtstart, datetime.time(0, 0, 0)).astimezone(pytz.UTC) end = datetime.datetime.combine(self.dtend, datetime.time(0, 0, 0)).astimezone(pytz.UTC) cal = self.__prepare_icalendar() event = self.__prepare_event(subject, html_content, start, end) alarm = self.__prepare_alarm() # Add a reminder event.add_component(alarm) cal.add_component(event) part = self.__prepare_invite_blocker(cal) msg = self.__prepare_email_message(subject, html_content) msg.attach(part) # Send the email out - 直接传入原始收件人列表 self.smtp.sendmail(msg["From"], self.receivers, msg.as_string()) self.smtp.quit() print('Invitation sent out') def main(): receivers = ['test1@test.com', 'test2@test.com', 'test3@test.com'] cluster_name = 'TEST NOW (test_now)' # test cluster name email_writer = EmailWriter( receivers, cluster_name, datetime.datetime.strptime('2023-02-16', '%Y-%m-%d').date(), datetime.datetime.strptime('2023-02-16', '%Y-%m-%d').date() + datetime.timedelta(days=1), '100% Availability', tasks='Minor test' ) print('Sending email') email_writer.send_appointment() if __name__ == '__main__': main()
内容的提问来源于stack exchange,提问作者mayer
相关产品推荐
相关产品推荐

