I have a script that will run periodically to send email invitations to all receivers to inform them about upcoming maintenance. Here is the code example
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 = '[email protected]'
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 = ['[email protected]', '[email protected]', '[email protected]']
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()
However, when I tested the code, I could see only the first recipient in the receivers list can get the outlook invitation. How to fix the code to let all email account in the list can get the invitation?
Looking at some other examples, it looks like the msg['To'] object needs to be in a string format with a delimiter of ',' I believe you are using ';' try changing that in your code and see if that resolves the issue.
current_code: msg['To'] = ';'.join(self.receivers)
new_code: msg['To'] = ', '.join(self.receivers)