ssmtp for sending emails from Yun (mini)

I'm trying to set up my arduino yun mini to send emails. I got Temboo to work but the free version has too many limitations so I'm trying to get something else to work.

I set up an @outlook.com email to use and installed the ssmtp package on the yun. I edited the ssmtp.conf file to read as follows:

root=username@outlook.com
mailhub:587=smtp-mail.outlook.com
rewriteDomain=outlook.com
Hostname=ardyun.local
UseSTARTTLS=YES
AuthUser=username@outlook.com
AuthPass=password
AuthMethod=LOGIN

During the initial config process, i named by board "ardyun". The error I get is telling me that ardyun.local is an invalid domain name (i have tried many others including the ip address of the board, "localhost", "username@outlook.com").

I'm not sure if this is related, but typing "ardyun.local/" into a browser does not work. I have to type out the board's actual IP address in order to connect. Maybe fixing that issue would fix my ssmtp issue as well. Opening a windows command prompt and typing "ping ardyun.local" also does not work, but "ping 192.168.50.25" does (arduino's local ip).

Any help would be appreciated! (Also, I'm not tied to ssmtp if you know of another way to send emails, but Temboo automatically changes my appkey every month and that is very annoying.)

Thanks!
-Ben

benfreudberg:
I'm not sure if this is related, but typing "ardyun.local/" into a browser does not work.

I'm not sure, but I'm guessing it's not related. For ardyun.local to work, you need to have an mDNS client on your computer (mDNS is also called ZeroConf.) Macs and iOS devices have such a client installed by default. Windows does not, but it can be installed by downloading and installing Apple's "Bonjour Print Services" package, or by downloading and installing iTunes (or probably a few other Apple software packages.) Linux needs to have Avahi installed (that's from memory, the name might not be right.) Last I heard, there was no package available for Android devices.

To answer your mail question, I've not tried ssmtp. I've had good luck using smtplib in Python to send mail, and poplib to read it. I'm sorry, but I don't recall if they are part of the default Python installation, or if there was something I had to install. I wrote the attached Python code as a wrapper to those libraries. Near the beginning of the file is a set of configuration statements you will need to update to match your servers and credentials:

POPSERVER = 'pop.some_domain.com'       # Update with your POP server's name (incoming mail server
SMTPSERVER = 'smtp.some_domain.com'     # Update with your SMTP server's name (outgoing mail server)
USERNAME = 'yun@some_domain.com'        # Update with your login name for the above servers
PASSWORD = 'its_a_secret'               # Update with the password for the above servers
EMAIL = 'Yun <yun@some_domain.com>'     # Update with the sender name and email address

To use this module in your Python code, you need to import yunEmail, and then call the sendEmail function. For example:

# Send a message via email
#
# Parameters:
#    msg     - body of message
def sendAlert(msg):
   if (not yunEmail.sendEmail( sender  = 'sender@some_domain.com',
                               to      = ['somebody@somewhere.com'],
                               subject = 'Alert Message',
                               body    = msg)):
       print "Unable to send message: " + msg

Note that the "to" parameter is actually an array, you can send to multiple people using something like:
to = ['somebody@somewhere.com', 'someone_else@somewhere_else.com'],

To receive mail, you simply call receiveEmail, passing a callback function as a parameter. This will read all incoming mail in the mailbox, and call the callback function for each one. For example:

# Callback function to process received emails
# Called for each individual email received
#
# Parameters:
#    sender    - Sender of received message
#    subject   - Subject of receved message
#    body      - Body of received message
def processEmailMessage(sender, subject, body):
   db.logActivity('Email received:
From: {0}
Subj: {1}
Body: {2}'.format(sender, subject, body))
   resp = []

   if ('<HELP>' in body):
      resp.append('Available commands:')
      resp.append('   <HELP>     - This listing')
      resp.append('   <STATUS>   - Current system status')
      resp.append('   <ON>       - Turn output on')
      resp.append('   <OFF>      - Turn output off')
      resp.append('')

   if ('<STATUS>' in body):
      resp.append('Current system status')
      status = db.getStatusStrs()
      keys = status.keys()
      keys.sort()
      for key in keys:
         resp.append('   {0:>20}: {1}'.format(key, status[key]))
      resp.append('')

   if ('<ON>' in body):
      db.setCommand('On')
      resp.append('Light temporarily turned on')
      resp.append('')

   if ('<OFF>' in body):
      db.setCommand('Off')
      resp.append('Light temporarily turned off')
      resp.append('')

   if (len(resp) > 0):
      # Send an email back with the response
      if (not yunEmail.sendEmail( to      = [sender],
                                  subject = 'Re: ' + str(subject),
                                  body    = '\n'.join(resp))):
         db.logActivity("Unable to send email response")

   #return (len(resp) > 0)
   return True





# The beginning of the main code
db = sunsetData.sunData()

db.logActivity("Email handler startup")

lastMailCheckTime = 0
mailCheckInterval = 60

# A loop that repeats forever
while (True):

   # Check if it's time to process emails
   now = time.time()
   if ((now - lastMailCheckTime) > mailCheckInterval):
      lastMailCheckTime = now
      yunEmail.receiveEmail(processEmailMessage)

   # Sleep for a while before checking again
   time.sleep(5)

In this example, db is a module that contains a small database of the application state, and provides functions to get status, accept commands, and log activity. This is very much application dependent, and you will likely want to do something completely different. This is just an example of how you can receive emails, react to the contents of the email, and send a response. This is a shortened example, I use this to be able to check on the status of the application running on the Yun, and send it commands, when I am not on the local network and can't access the Yun directly. This way, I can send it emails from anywhere in the world to check in on it. This makes use of a mailbox that is dedicated to the Yun, it's the only thing sending/receiving emails through that mailbox.

All of this is in Python, in a set of scripts that are called from the Yun sketch using the Bridge Library's Process class. This is not a finished set of code, but it should give you some other ideas, and maybe give you a head start to a working solution. (I did make some minor edits to the code to simplify it a bit, and remove confidential information. I didn't compile it after the edits, so it's possible I introduced some minor bugs. It may need some minor corrections if I did something stupid in the edits.)

Note, the forum doesn't allow attaching .py files, so I've renamed it with a .txt extension for the sake of attaching it. It should be renamed to yunEmail.py when putting it on a Yun.

yunEmail.py.txt (3.75 KB)

Thank you! I got it working using the information you gave me as well as a few bits and pieces from around the web.

I'll share what I have for anyone who comes across this post:

I created a python file called yunEmail.py with the following code (you will need to supply your own email and password. For gmail, i think you have to enable 2 factor authentication and use the password that you create during that process, not your normal password):

from email.MIMEMultipart import MIMEMultipart
from email.MIMEText import MIMEText
import smtplib


def sendEmail(toaddr, subjectstr, bodystr):
    fromaddr = "*****@gmail.com"
    psswrd = "*****"

    msg = MIMEMultipart()
    msg["From"] = fromaddr
    msg["To"] = toaddr
    msg["Subject"] = subjectstr
    
    body = MIMEText(bodystr)
    msg.attach(body)
    
    try:
        server = smtplib.SMTP('smtp.gmail.com', 587)
        server.ehlo()
        server.starttls()
        server.ehlo()
        server.login(fromaddr, psswrd)
        server.sendmail(msg["From"], msg["To"].split(","), msg.as_string())
        server.quit()
        return True
    except:
        return False

I created a second python file called sendMail.py:

import yunEmail


bodyfile = open("/mnt/bodytext.txt","r")
bodystr = bodyfile.read()
bodyfile.close()

addrfile = open("/mnt/recipients.txt","r")
recipstr = addrfile.read()
addrfile.close()

subfile = open("/mnt/subject.txt","r")
subjectstr = subfile.read()
subfile.close()


result = yunEmail.sendEmail(recipstr, subjectstr, bodystr)
if result == True:
    print "email succesfully sent"
else:
    print "email failed to send"

I also created 3 blank text files:
recipients.txt
subject.txt
bodytext.txt

I put all 5 of these files in the root/mnt directory on the yun (the exact directory shouldn't matter, just make sure it matches everywhere relevant in the code).

The arduino code looks like this:

#include <FileIO.h>
#include <Process.h>

int sensorValue=345;

void setup() {
 Bridge.begin();  // Initialize the Bridge
 Serial.begin(9600);  // Initialize the Serial
 FileSystem.begin();
 // Wait until a Serial Monitor is connected.
 while(!Serial);
}

void loop() {
  String subject = "arduino subject";
  String recipients = "address1@gmail.com,address2@gmail.com";
  String body = "this is the body\n";
  body+= "here is a second line in the body\n";
  body+= "and another, how bout the sensor value? Here it is: ";
  body+= sensorValue;
  body+= "\n\nFrom,\nArduino";
  
  sendEmail(subject,recipients,body);
  
  while(1); //only send one email
}

void sendEmail(String subject, String recipients,String body)
{
 Process p;

 File subjectfile = FileSystem.open("/mnt/subject.txt", FILE_WRITE);
 // if the file is available, write to it:
 if (subjectfile) {

   subjectfile.print(subject); //has to be print, not println
   subjectfile.close();
   // print to the serial port too:
   Serial.println(subject);
 }  
 // if the file isn't open, pop up an error:
 else {
   Serial.println("error opening subject.txt");
 }

 File recipientsfile = FileSystem.open("/mnt/recipients.txt", FILE_WRITE);
 // if the file is available, write to it:
 if (recipientsfile) {

   recipientsfile.print(recipients); //has to be print, not println
   recipientsfile.close();
   // print to the serial port too:
   Serial.println(recipients);
 }  
 // if the file isn't open, pop up an error:
 else {
   Serial.println("error opening recipients.txt");
 }

 File bodyfile = FileSystem.open("/mnt/bodytext.txt", FILE_WRITE);
 // if the file is available, write to it:
 if (bodyfile) {

   bodyfile.print(body);
   bodyfile.close();
   // print to the serial port too:
   Serial.println(body);
 }  
 // if the file isn't open, pop up an error:
 else {
   Serial.println("error opening bodytext.txt");
 }

 p.runShellCommand("python /mnt/sendMail.py");

 while(p.running());  

 while (p.available()>0) {
   char c = p.read();
   Serial.print(c);
 }

 // Ensure the last bit of data is sent.
 Serial.flush();
}

This method is a bit messy. I write subject, recipients, and body to files on the yun using arduino, and then my python script reads those files. Maybe there is a way to call a python function with arduino, but I don't know how. This workaround allows you to just call a python script though, so that's good enough for me.

benfreudberg:
Maybe there is a way to call a python function with arduino, but I don't know how.

I don't know of a way to call a function, just a script. But there are a few different ways to call a script using the Process class:

  • runShellCommand() - This method starts an instance of the command shell, passes it the command line, and waits for the process to terminate. The command shell interprets and processes the command line. This is a good option when you want to run an actual shell command, but it introduces a bit of overhead when you are simply running an executable file like Python. Note that calling the running() method in a while loop after this style of invocation doesn't really accomplish anything, as by definition runShellCommand() doesn't return until the process is complete (if you look at the source code for the function, it includes its own while running loop.)
  • run() - this method is a little different than the previous method, in that it doesn't spin up a command interpreter shell, it just launches the specified executable directly. This saves a little processing overhead. It is ideal where the command line consists of a simple executable image and parameters. It does not work if the command line is a shell command that is not a separate executable (like 'ls' or 'rm'.) Like runShellCommand(), it does not return until the process is complete.
  • runAsynchronously() - this method is similar to run(), except that it doesn't wait for the process to complete. (There is also runShellCommandAsunchronously().) It's a bit more work to manage the asynchronous nature, but it comes with a couple big benefits: if the process is lengthy, the sketch can keep on doing other things, and it allows two-way communications with the process.

It's that last feature of runAsynchronously() that gives it a lot of power: whatever the sketch writes to the process object will get sent to the Linux process's STDIN, and whatever the process prints to STDOUT can be read by the sketch. The Process class derives from the Stream class, so you can use any of the writing (print) methods and any of the read methods that you would use with the Serial class.

I normally try to do as much processing on the Linux side as possible, and my typical pattern is to launch an asynchronous process at the beginning of the sketch, and then use two way communications throughout the life of the application. You could do that here, but in your case, you might want to consider the pattern where you launch the process to send a single message, and it terminates when the message completes. I see why you created files with the various message details, because that's difficult to pass on the command line. But rather than writing the files, you could send that data through the Process object. The calling sequence could be something like:

  • Call p.runAsunchronously("python -u /mnt/sendMail.py"); to start the process. The "-u" option makes Python run in unbuffered mode, which improves two way communications (without it, Python won't return anything to the sketch until it fills a very large buffer, or the process terminates.)
  • Enter a loop, and call p.println(recipientString); to send each recipient to the script, each on their own line.
  • When done sending recipients, send a blank line to the script by calling p.println();.
  • Send the subject to the script by calling p.println(subjectString);
  • Send the message body to the script by calling p.println for each line.
  • Send an end of message terminator to the script, something that wouldn't appear in the message, perhaps "~" on a line by itself: p.println("~");
  • Wait for the script to send the mail with a while(p.running()); loop
  • Use p.available() and p.read() to read the success/failure status from the script

The corresponding Python script would start out by reading lines of input, and adding each to the recipient list, stopping when it sees a blank line. It then reads the next line and uses that as the subject. It then reads in lines of the message body until it sees the end of message terminator. At that point, it has all of the recipient/subject/message data, and can send the mail in the same manner as you are doing now.

If you are happy with what you have, none of this needs to be done. The advantages to doing something like this are primarily efficiency, and the opportunity to learn something new. Writing files from the sketch using the Bridge library classes is quite slow and inefficient, and it incurs disk access which introduces the possibility of file I/O errors. By sending the data directly from the sketch to the script, you minimize the overhead, and you eliminate the file system involvement.

Just some thoughts to consider...