0

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 = '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()

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?

mayer
  • 77
  • 1
  • 7

2 Answers2

1

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)

Nolan Walker
  • 352
  • 1
  • 7
  • feel free to check the solutions on this stackoverflow link: https://stackoverflow.com/questions/8856117/how-to-send-email-to-multiple-recipients-using-python-smtplib – Nolan Walker Feb 16 '23 at 03:02
  • The [SMTP documentation](https://docs.python.org/3/library/smtplib.html#smtplib.SMTP.sendmail) shows a `,` delimiter as well – Shorn Feb 16 '23 at 03:06
  • Thank you for your comment! I tried it to replace the delimiter from ```;``` to ```,```, but unfortunately, still the first recipient can get the outlook invitation blocker. – mayer Feb 16 '23 at 03:54
  • in your sendemail() function call, you are passing in [msg['To']], try changing it to msg['To'].split(","). All you did was pass the entire string into the array. I believe you need to split the emails out of the string and populate the array – Nolan Walker Feb 16 '23 at 06:09
0

Thank you for your help!

Finally, I got a solution here. First, as the above answer said, I need to use , as the delimiter.

msg['To'] = ', '.join(self.receivers)

Second, in the function of smtp.sendmail(), the receiver parameter type is a list, so here I directly give the function a list

self.smtp.sendmail(msg['From'], self.receivers, msg.as_string())

Source code of sendmail()

    def sendmail(self, from_addr, to_addrs, msg, mail_options=(),
                 rcpt_options=()):
        """This command performs an entire mail transaction.

        The arguments are:
            - to_addrs     : A list of addresses to send this mail to.  A bare
                             string will be treated as a list with 1 address.

 Example:

         >>> import smtplib
         >>> s=smtplib.SMTP("localhost")
         >>> tolist=["one@one.org","two@two.org","three@three.org","four@four.org"]
         >>> msg = '''\\
         ... From: Me@my.org
         ... Subject: testin'...
         ...
         ... This is a test '''
         >>> s.sendmail("me@my.org",tolist,msg)
mayer
  • 77
  • 1
  • 7