Search code examples
pythonpython-3.xemailgmail

Python: Error sending more than one email in a row


I have made a development who let me to send emails. If I try to send an email to one receipt there is no problem. But, If I try to send the same email to more than one receipts then I've got an error. I don't want to send an email to several receipts, what I want is to send one email to each of the receipts.

The error that I've got is:

  File "/home/josecarlos/Workspace/python/reports/reports/pregame.py", line 22, in __init__
    self.send_mail(subject, message, fileName)
  File "/home/josecarlos/Workspace/python/reports/reports/report.py", line 48, in send_mail
    message = mail.create_message()
  File "/home/josecarlos/Workspace/python/reports/com/mail/mail.py", line 100, in create_message
    message.attach(MIMEText(self.params["message"], "plain"))
  File "/usr/lib/python3.6/email/mime/text.py", line 34, in __init__
    _text.encode('us-ascii')
AttributeError: 'dict' object has no attribute 'encode'

To send an email I have this class:

import base64
import logging
import os
import os.path
import pickle
from email import encoders
from email.mime.text import MIMEText
from email.mime.multipart import MIMEMultipart
from email.mime.base import MIMEBase
from google_auth_oauthlib.flow import InstalledAppFlow
from google.auth.transport.requests import Request
from googleapiclient import errors
from googleapiclient.discovery import build


class Mail:
    def __init__(self, params):
        '''
        :param params: It's a dictionary with these keys:
        from: Email account from the email is sended
        to: Email account who will receive the email
        subject: Subject of the email
        message: Message of the email.
        game: Next games
        '''
        self.params = params

    @staticmethod
    def get_service():
        """Gets an authorized Gmail API service instance.

        Returns:
            An authorized Gmail API service instance..
        """

        # If modifying these scopes, delete the file token.pickle.
        SCOPES = [
            #'https://www.googleapis.com/auth/gmail.readonly',
            'https://www.googleapis.com/auth/gmail.send',
        ]
        creds = None
        # The file token.pickle stores the user's access and refresh tokens, and is
        # created automatically when the authorization flow completes for the first
        # time.
        if os.path.exists('token.pickle'):
            with open('token.pickle', 'rb') as token:
                creds = pickle.load(token)
        # If there are no (valid) credentials available, let the user log in.
        if not creds or not creds.valid:
            if creds and creds.expired and creds.refresh_token:
                creds.refresh(Request())
            else:
                flow = InstalledAppFlow.from_client_secrets_file(
                    'com/mail/credentials.json', SCOPES)
                creds = flow.run_local_server(port=0)
            # Save the credentials for the next run
            with open('token.pickle', 'wb') as token:
                pickle.dump(creds, token)
        service = build('gmail', 'v1', credentials=creds)
        return service

    @staticmethod
    def send_message(service, sender, message):
      """Send an email message.

      Args:
        service: Authorized Gmail API service instance.
        sender: User's email address. The special value "me"
        can be used to indicate the authenticated user.
        message: Message to be sent.

      Returns:
        Sent Message.
      """
      try:
        sent_message = (service.users().messages().send(userId=sender, body=message)
                   .execute())
        logging.info('Message Id: %s', sent_message['id'])
        return sent_message
      except errors.HttpError as error:
        logging.error('An HTTP error occurred: %s', error)

    def create_message(self):
        """Create a message for an email.

        Args:
        sender: Email address of the sender.
        to: Email address of the receiver.
        subject: The subject of the email message.
        message_text: The text of the email message.

        Returns:
        An object containing a base64url encoded email object.
        """
        #message = MIMEText(message_text)
        message = MIMEMultipart()
        message['from'] = self.params["from"]
        message['to'] = self.params["to"]
        message['subject'] = self.params["subject"]
        message.attach(MIMEText(self.params["message"], "plain"))
        routeFile = self.params["routeFile"] + self.params["fileName"]
        fileName = self.params["fileName"]
        # Open PDF file in binary mode
        with open(routeFile, "rb") as attachment:
            # Add file as application/octet-stream
            # Email client can usually download this automatically as attachment
            part = MIMEBase("application", "octet-stream")
            part.set_payload(attachment.read())

        # Encode file in ASCII characters to send by email
        encoders.encode_base64(part)
        # Add header as key/value pair to attachment part
        part.add_header(
            "Content-Disposition",
            f"attachment; filename= {fileName}",
        )
        # Add attachment to message and convert message to string
        message.attach(part)
        s = message.as_string()
        b = base64.urlsafe_b64encode(s.encode('utf-8'))
        return {'raw': b.decode('utf-8')}

To send an email, I'll do it with this method:

def send_mail(self, subject, message, fileName):
    args = self.params["destiny"]
    if self.params["competition"] == COMPETITIONS.LF1 or self.params["competition"] == COMPETITIONS.LF2:
         data = SearchData(args, "subscriptors.emails=")
    else:
         data = SearchDataFIBA(args, "subscriptors.emails=")
    emails = data.get_result().getData()

    for item in emails:
         print(f"Enviamos informe a la cuenta: {item['email']}")
         params = {
             "from" : "[email protected]",
             "to" : item["email"],
             "subject": subject,
             "message" : message,
             "fileName" : fileName,
             "routeFile" : f"output/reports/{self.params['destiny']}/"
        }
        mail = Mail(params)
        message = mail.create_message()
        service = mail.get_service()
        mail.send_message(service, "[email protected]", message)

My application has secure access to google account.

I don't know how I cannot send more than an email in a row when I have no

problem to send only one e-mail.

Am I doing something wrong?

Edit I:

To reproduce the error, you can test it with this test code:

import unittest
import os
from com.mail.mail import Mail


class TestSendMail(unittest.TestCase):
    def setUp(self) -> None:
        os.chdir(os.path.abspath(os.path.join(os.path.dirname( __file__ ), '..')))
def test_send_mail(self):
    message = "¡¡¡Hola!!!\n\nOs enviamos el informe pre partido previo a vuestro próximo partido.\n\nSaludos,\n\nBasketmetrics.com"
    subject = "Informe pre partido"
    fileName = "name_of_the_file"
    emails = [{"email" : "[email protected]"}, {"email" : "[email protected]"}]

    for item in emails:
        print(f"Enviamos informe a la cuenta: {item['email']}")
        params = {
            "from" : "[email protected]",
            "to" : item["email"],
            "subject": subject,
            "message" : message,
            "fileName" : fileName,
            "routeFile" : "route to the file"
        }
        mail = Mail(params)
        message = mail.create_message()
        service = mail.get_service()
        mail.send_message(service, "[email protected]", message)

Also, you have to change some values for your own values and the file credentials.json file of your own google account

Edit II:

I have found where it produces the error but not why. The problem comes up when I invoque for second time the class Mail. In that moment I pass some parameters to the constructor with params variable. In that variable I pass the text of the message. This message is create outside of the loop.

If I read the first 120 characters of what receives the Mail class in params["message"] in the constructor:

def __init__(self, params):
    '''
    :param params: It's a dictionary with these keys:
    from: Email account from the email is sended
    to: Email account who will receive the email
    subject: Subject of the email
    message: Message of the email.
    game: Next games
    '''
    self.params = params
    print(f"message received: {params['message'][:120]}")

For the first time, I've got the content of message variable:

message received: ¡¡¡Hola!!!

Os enviamos el informe pre partido previo a vuestro próximo partido.

Saludos,

Basketmetrics.com

But the second time, I should receive the same text!!! But I receive an error:

Error
Traceback (most recent call last):
  File "/usr/lib/python3.6/unittest/case.py", line 59, in testPartExecutor
    yield
  File "/usr/lib/python3.6/unittest/case.py", line 605, in run
    testMethod()
  File "/home/josecarlos/Workspace/python/reports/test/test_send_mail.py", line 26, in test_send_mail
    mail = Mail(params)
  File "/home/josecarlos/Workspace/python/reports/com/mail/mail.py", line 27, in __init__
    print(f"message received: {params['message'][:120]}")
TypeError: unhashable type: 'slice'

If I read all the message without any limit or characers. For the first time, I receive the content of the variable message. But in the second time, I receive a very big string os characters, here is a little example:

lGWFBVZzhQc3pDNWdiNmhnMW1odDZHcmlFZWsyClZvTTBRT1R2dXpWaWlyZkpleS9mZXhtR3V3V2hTV0JjWERtSUNnWENTQVJ1QjdrN1Nzd3BrZ0c1Rkl3MXVDMmNyZk95ZUhySVM1dHQKSUh2T1YvWW1Pd2YzL3B2WEpLaEMza

Why I receive this string of charanters instead of the value of message variable?

I have check it that If I put message variable inside the for loop, instead of outside of the loop ... It works!!! I receive the two mails!!!

But, this solution isn't usefull because I want to reuse my code and I need to pass some values through variables.

So, why in the second time I don't receive the value of message variable and I receive a long string of characters?

How can I fix this error? Why it happens this error?

Edit III:

Checking the type of the value that I receive in the constructor of Mail, for the first time is "string":

typeof: <class 'str'>

But in the second time is "dict":

typeof: <class 'dict'>

And checking the keys of self.params["message"] are:

keys: dict_keys(['raw'])

I dont't understand anything ... How is it possible that params["message"] has the value of message variable and the second time params["message"] has modified its type to raw?

Edit IV:

I have modified the content of message variable from ...

message= ""

To ...

mesage = "¡¡¡Hola!!!0x0a0x0aOs enviamos el informe pre partido previo a vuestro próximo partido.0x0a0x0aSaludos,0x0a0x0aBasketmetrics.com"

But it doesn't work. I've got the same error.

Edit V:

I have modified the contento of message variable. Now, instead of plain text, I'm going to send an html ...

    message = """\
    <html>
        <head></head>
        <body>
            <p>
                Hi, this is a test!!!
            </p>
            <p>
                Best regards!!!
            </p>
        </body>
    </html>
    """

To do send this message, you have to modify this instruction in method create_message of Mail class:

message.attach(MIMEText(self.params["message"], "plain"))

to this:

message.attach(MIMEText(self.params["message"], "html"))

And ... I've got the same error!!!

I don't know what to do anymore ...

Edit VI:

Last attempt ... I have modified the text of the message removing "strange" character like "" or "<" and I have send a simple text with the message "Hello".

So, my message variable now is:

message = "Hello"

And I have modified again the format of the email, from "html" to "plain"

And ... I've got the same error in my second email!!!

This is frustrating ... :((((((


Solution

  • You have:

    def send_mail(self, subject, message, fileName):
    

    Where argument message is the message text to be sent. But we have in your function `test_send_mail':

    def test_send_mail(self):
        message = "¡¡¡Hola!!!\n\nOs enviamos el informe pre partido previo a vuestro próximo partido.\n\nSaludos,\n\nBasketmetrics.com"
        # code omitted
    
        for item in emails:
            # code omitted
            message = mail.create_message() # overlaying message text with return value from create_message
            service = mail.get_service()
            mail.send_message(service, "[email protected]", message)
    

    In the for item in emails: loop, you have overlaid message with the return value from the call to mail.create_message(), so for the next iteration message is not a string anymore. This, I believe is your problem. You need to use a different variable name for either the return value from the call to mail.create_message() or for the message text.