smtp_client.cpp

Go to the documentation of this file.
00001 #ifndef SMTP_CLIENT_IMPLEMENTATION_FILE
00002 #define SMTP_CLIENT_IMPLEMENTATION_FILE
00003 
00004 /*****************************************************************************\
00005 *                                                                             *
00006 *  Name   : smtp_client                                                       *
00007 *  Author : Chris Koeritz                                                     *
00008 *                                                                             *
00009 *******************************************************************************
00010 * Copyright (c) 2006-$now By Author.  This program is free software; you can  *
00011 * redistribute it and/or modify it under the terms of the GNU General Public  *
00012 * License as published by the Free Software Foundation; either version 2 of   *
00013 * the License or (at your option) any later version.  This is online at:      *
00014 *     http://www.fsf.org/copyleft/gpl.html                                    *
00015 * Please send any updates to: fred@gruntose.com                               *
00016 \*****************************************************************************/
00017 
00018 #include "smtp_client.h"
00019 
00020 #include <basis/function.h>
00021 #include <basis/istring.h>
00022 #include <basis/log_base.h>
00023 #include <basis/mutex.h>
00024 #include <basis/portable.h>
00025 #include <basis/string_array.h>
00026 #include <data_struct/static_memory_gremlin.h>
00027 #include <mechanisms/time_stamp.h>
00028 #include <opsystem/byte_filer.h>
00029 #include <loggers/console_logger.h>
00030 #include <opsystem/path_configuration.h>
00031 #include <opsystem/redirecter.h>
00032 #include <opsystem/registry_config.h>
00033 
00034 #ifdef __WIN32__
00035   #include <atlenc.h>
00036 #endif
00037 
00038 #ifndef OMIT_PROGRAM_WIDE_LOGGER
00039   #define LOG(s) CLASS_EMERGENCY_LOG((_logging? *_logging \
00040       : program_wide_logger()), s)
00041 #else
00042   #define LOG(s) CLASS_EMERGENCY_LOG(console_logger(), s)
00043 #endif
00044 
00045 #define CHECK_STILL_RUNNING(to_return) \
00046   if (!msmtp.running()) { \
00047     /* for some reason, it has departed. */ \
00048     LOG("the msmtp application already exited."); \
00049     if (!okay_exit_value(msmtp.exit_value())) return to_return; \
00050     else return OKAY; \
00051   }
00052 
00053 const int MAXIMUM_WRITE_ITERATIONS = 40;
00054   // we won't try to write the message body forever, but we will try this
00055   // many times to get it all into the email.
00056 
00057 const int MAXIMUM_PAUSE_FOR_SENDING = 30 * SECOND_ms;
00058   // we will allow the mail program to take this long to deal with the
00059   // email and exit.
00060 
00061 const int DEFAULT_TIMEOUT = 0;
00062   // the default is to never timeout, rather than zero timeout.
00063 
00064 smtp_client::smtp_client()
00065 : _logging(NIL),
00066   _sess_server(new istring),
00067   _sess_port(SC_SMTP_DEFAULT_PORT),
00068   _sess_user(new istring),
00069   _sess_password(new istring),
00070   _msg_sender(new istring),
00071   _msg_subject(new istring),
00072   _msg_body(new istring),
00073   _msg_attachments(new string_array),
00074   _msg_recipients(new string_array),
00075   _msg_carbons(new string_array),
00076   _timeout(DEFAULT_TIMEOUT)
00077 {}
00078 
00079 smtp_client::smtp_client(log_base &logging)
00080 : _logging(&logging),
00081   _sess_server(new istring),
00082   _sess_port(SC_SMTP_DEFAULT_PORT),
00083   _sess_user(new istring),
00084   _sess_password(new istring),
00085   _msg_sender(new istring),
00086   _msg_subject(new istring),
00087   _msg_body(new istring),
00088   _msg_attachments(new string_array),
00089   _msg_recipients(new string_array),
00090   _msg_carbons(new string_array),
00091   _timeout(DEFAULT_TIMEOUT)
00092 {}
00093 
00094 smtp_client::~smtp_client()
00095 {
00096   WHACK(_sess_server);
00097   WHACK(_sess_user);
00098   WHACK(_sess_password);
00099   WHACK(_msg_sender);
00100   WHACK(_msg_subject);
00101   WHACK(_msg_body);
00102   WHACK(_msg_attachments);
00103   WHACK(_msg_recipients);
00104   WHACK(_msg_carbons);
00105 }
00106 
00107 const char *smtp_client::outcome_name(const outcome &to_name)
00108 { return communication_commons::outcome_name(to_name); }
00109 
00110 void smtp_client::setup_session(const istring &server_name,
00111     int smtp_port, const istring &user_name, const istring &password)
00112 {
00113   FUNCDEF("setup_session");
00114   set_server(server_name);
00115   set_port(smtp_port);
00116   set_auth_user(user_name);
00117   set_auth_password(password);
00118 
00119 #ifdef __WIN32__
00120   // we must write the .netrc file on windows because the msmtp app is not
00121   // reading from normal stdin.  instead, it insists on ignoring stdin and
00122   // reading from the windows console input buffer.  that handle for the
00123   // console input seems to not be inheritable unless your launching app
00124   // is also console mode, which we totally cannot depend on.  thus we
00125   // resort to this lame approach.
00126   registry_configurator reg(registry_configurator::hkey_current_user,
00127       registry_configurator::RETURN_ONLY);
00128 
00129   istring temp_home = reg.load("Software\\Microsoft\\Windows\\CurrentVersion"
00130       "\\Explorer\\User Shell Folders", "AppData", "");
00131 //LOG(istring("original home location is \"") + temp_home + "\"");
00132   if (!temp_home) {
00133     // assume we are operating as the system service.  this is not a great
00134     // assumption, but it does seem like we never get a path for the
00135     // system account.
00136     temp_home = "C:/Documents and Settings/LocalService/Application Data";
00137   }
00138 
00139 //LOG(istring("home location is ") + temp_home);
00140 
00141   istring password_line("machine ");
00142   password_line += *_sess_server;
00143   password_line += " login ";
00144   password_line += *_sess_user;
00145 
00146   istring line_to_seek = password_line;
00147     // this part of the line forms a pattern we need to remove if it's
00148     // already in the file.
00149 
00150   password_line += " password ";
00151   password_line += *_sess_password;
00152   password_line += log_base::platform_ending();
00153   byte_filer netrc(temp_home + "/netrc.txt", "r");
00154   istring full_file;
00155   // read the current version of the file now.
00156   while (!netrc.eof()) {
00157 const int MAX_LINE = 1000;
00158     istring line_read;
00159     int chars_read = netrc.getline(line_read, MAX_LINE);
00160     if (chars_read) {
00161       if (line_read.contains(line_to_seek)) continue;
00162         // skip the previous version of the password line.
00163       full_file += line_read;
00164     }
00165   }
00166   netrc.close();
00167   netrc.open(temp_home + "/netrc.txt", "w");
00168   full_file += password_line;  // add new line to the contents.
00169   // stream file back out, minus line with prefix we are seeking, plus
00170   // the new password setting.
00171   netrc.write(full_file);
00172 //LOG(istring("wrote file ") + netrc.filename());
00173   netrc.close();
00174 #endif
00175 }
00176 
00177 void smtp_client::set_server(const istring &server_name)
00178 { *_sess_server = server_name; }
00179 
00180 void smtp_client::set_port(int smtp_port) { _sess_port = smtp_port; }
00181 
00182 void smtp_client::set_auth_user(const istring &user_name)
00183 { *_sess_user = user_name; }
00184 
00185 void smtp_client::set_auth_password(const istring &password)
00186 { *_sess_password = password; }
00187 
00188 void smtp_client::setup_message(const istring &sender_email,
00189     const istring &subject, const istring &message_body,
00190     const istring &one_recipient)
00191 {
00192   set_sender(sender_email);
00193   set_subject(subject);
00194   set_body(message_body);
00195   clear_recipients();
00196   add_recipient(one_recipient);
00197   clear_carbons();
00198 }
00199 
00200 void smtp_client::add_recipient(const istring &recipient)
00201 {
00202   if (!recipient) return;
00203   *_msg_recipients += recipient;
00204 }
00205 
00206 bool smtp_client::remove_recipient(const istring &recipient)
00207 {
00208   if (!recipient) return false;
00209   bool found_one = false;
00210   for (int i = 0; i < _msg_recipients->length(); i++) {
00211     // try to find the filename in the list.
00212     if (_msg_recipients->get(i) == recipient) {
00213       // found it; now whack it.
00214       _msg_recipients->zap(i, i);
00215       i--;  // skip the loop one back for what we just removed.
00216       found_one = true;
00217     }
00218   }
00219   return found_one;
00220 }
00221 
00222 void smtp_client::clear_recipients()
00223 {
00224   _msg_recipients->reset();
00225 }
00226 
00227 void smtp_client::add_carbon(const istring &carbon)
00228 {
00229   if (!carbon) return;
00230   *_msg_carbons += carbon;
00231 }
00232 
00233 bool smtp_client::remove_carbon(const istring &carbon)
00234 {
00235   if (!carbon) return false;
00236   bool found_one = false;
00237   for (int i = 0; i < _msg_carbons->length(); i++) {
00238     // try to find the filename in the list.
00239     if (_msg_carbons->get(i) == carbon) {
00240       // found it; now whack it.
00241       _msg_carbons->zap(i, i);
00242       i--;  // skip the loop one back for what we just removed.
00243       found_one = true;
00244     }
00245   }
00246   return found_one;
00247 }
00248 
00249 void smtp_client::clear_carbons()
00250 {
00251   _msg_carbons->reset();
00252 }
00253 
00254 void smtp_client::set_sender(const istring &sender_email)
00255 {
00256   *_msg_sender = sender_email;
00257 }
00258 
00259 void smtp_client::set_subject(const istring &subject)
00260 {
00261 #ifdef __WIN32__
00262   // If there are any extended characters, then encode the subject
00263   // using "Q" encoding as defined in RFC 2047.
00264   if (GetExtendedChars(subject.s(), subject.length()) > 0)
00265   {
00266     int bufSize= QEncodeGetRequiredLength(subject.length(),
00267         ATL_MAX_ENC_CHARSET_LENGTH);
00268     istring encodedSubject;
00269     encodedSubject.pad(bufSize);
00270     QEncode((BYTE *)(subject.s()), subject.length(),
00271         encodedSubject.access(), &bufSize, "iso-8859-1");
00272     *_msg_subject = encodedSubject;
00273   }
00274   else
00275   {
00276     *_msg_subject = subject;
00277   }
00278 #else
00279   *_msg_subject = subject;
00280 #endif
00281 }
00282 
00283 void smtp_client::set_body(const istring &message_body)
00284 {
00285 #ifdef __WIN32__
00286   // encode the message body as "quoted-printable" as
00287   // defined in RFC 2045.  This will handle long lines and
00288   // any extended characters.
00289   int bufSize= QPEncodeGetRequiredLength(message_body.length());
00290   istring encodedBody;
00291   encodedBody.pad(bufSize);
00292   QPEncode((BYTE *)(message_body.s()), message_body.length(),
00293       encodedBody.access(), &bufSize);
00294   *_msg_body = encodedBody;
00295 #else
00296   *_msg_body = message_body;
00297 #endif
00298 }
00299 
00300 void smtp_client::set_timeout(int timeout)
00301 {
00302   _timeout = timeout;
00303 }
00304 
00305 /*
00306 void smtp_client::add_attachment(const istring &filename)
00307 {
00308   if (!filename) return;
00309   *_msg_attachments += filename;
00310 }
00311 
00312 bool smtp_client::remove_attachment(const istring &filename)
00313 {
00314   if (!filename) return false;
00315   bool found_one = false;
00316   for (int i = 0; i < _msg_attachments->length(); i++) {
00317     // try to find the filename in the list.
00318     if (_msg_attachments->get(i) == filename) {
00319       // found it; now whack it.
00320       _msg_attachments->zap(i, i);
00321       i--;  // skip the loop one back for what we just removed.
00322       found_one = true;
00323     }
00324   }
00325   return found_one;
00326 }
00327 
00328 void smtp_client::clear_attachments() { _msg_attachments->reset(); }
00329 
00330 */
00331 
00332 bool smtp_client::get_outputs(stdio_redirecter &msmtp, byte_array &feedback)
00333 {
00334   FUNCDEF("get_outputs");
00335   bool to_return = false;
00336   feedback.reset();
00337   outcome read_ret = msmtp.read(feedback);
00338   if (read_ret == stdio_redirecter::OKAY) {
00339     LOG(istring("out|") + istring(istring::UNTERMINATED,
00340         (char *)feedback.observe(), feedback.length()));
00341     to_return = true;
00342   }
00343   byte_array err_data;
00344   read_ret = msmtp.read_stderr(err_data);
00345   if (read_ret == stdio_redirecter::OKAY) {
00346     LOG(istring("err|") + istring(istring::UNTERMINATED,
00347         (char *)err_data.observe(), err_data.length()));
00348     feedback += err_data;
00349     to_return = true;
00350   }
00351   return to_return;
00352 }
00353 
00354 bool smtp_client::validate_address(const istring &to_check)
00355 {
00356   istring name_portion;
00357   istring email_addr = prune_address(to_check, name_portion);
00358   int indy = email_addr.find('@');
00359   if (non_positive(indy)) return false;
00360     // can't be missing an @ (negative return) and can't have the @ as the
00361     // first part (zero return).
00362   if (email_addr.length() - 1 == indy) return false;
00363     // can't be hiding the @ sign at the end of the string with no other text.
00364   int indy2 = email_addr.find('@', indy + 1);
00365   if (non_negative(indy2)) return false;
00366     // we can't have two @'s in a single email address.
00367 
00368 //hmmm: what about validating the actual textual characters of the address?
00369 //      what are the smtp constraints on those characters?
00370 // according to wikipedia, the constraints are:
00371 // According to RFC 2822, the local-part of the e-mail may use any of these ASCII characters:
00372 // Uppercase and lowercase letters (case insensitive)
00373 // The digits 0 through 9
00374 // The characters, ! # $ % & ' * + - / = ? ^ _ ` { | } ~
00375 // The character "." provided that it is not the first or last character in the local-part.
00376 //
00377 
00378   return true;
00379     // after all that, it's supposedly a tasty address.
00380 }
00381 
00382 istring smtp_client::prune_address(const istring &to_prune, istring &name)
00383 {
00384   name = "";
00385   istring pruned_sender = to_prune;
00386   int indy = pruned_sender.find("<");
00387   if (non_negative(indy)) {
00388     name = pruned_sender.substring(0, indy - 1);
00389     pruned_sender.zap(0, indy);
00390   }
00391   indy = pruned_sender.find(">");
00392   if (non_negative(indy)) {
00393     pruned_sender.zap(indy, pruned_sender.end());
00394   }
00395   return pruned_sender;
00396 }
00397 
00398 bool smtp_client::okay_exit_value(int value) { return value == 0; }
00399 
00400 #define CHECK_FOR_PASSWORD_PROMPT(bytes) { \
00401   istring text_gotten(istring::UNTERMINATED, (char *)bytes.observe(), \
00402       bytes.length()); \
00403   if (text_gotten.contains("password")) { \
00404     LOG("saw a password reference in the output; we may be missing " \
00405         "the password."); \
00406     msmtp.zap_program(); \
00407     return ACCESS_DENIED; \
00408   } \
00409   CHECK_STILL_RUNNING(BAD_INPUT); \
00410 }
00411 
00412 outcome smtp_client::send_email()
00413 {
00414   FUNCDEF("send_email");
00415   // set up the path to the msmtp application.
00416   istring msmtp_path = path_configuration::application_directory() + "/msmtp";
00417 #ifdef __WIN32__
00418   msmtp_path += ".exe";
00419 #endif
00420 
00421   istring text_name;
00422   istring pruned_sender = prune_address(*_msg_sender, text_name);
00423 
00424   // start accumulating command line parameters for it.
00425   isprintf smtp_parms("--protocol=smtp --host %s --port %d --from=%s",
00426       _sess_server->s(), _sess_port, pruned_sender.s());
00427 //LOG(istring("pruned sender: ") + pruned_sender);
00428 
00429   if (_timeout) {
00430     // the timeout was specified, so we enable it.  otherwise we expect the
00431     // default to be off, where it never times out.
00432     smtp_parms += isprintf(" --timeout=%d", _timeout / SECOND_ms);
00433   }
00434 
00435   // decide whether the session is to be authenticated or not.
00436   bool authenticating = false;
00437   if (_sess_user->t()) {
00438     authenticating = true;
00439     smtp_parms += " --auth=on";
00440     smtp_parms += isprintf(" --user=%s", _sess_user->s());
00441   }
00442 
00443   // add the recipients in now that all the other flags are done with.
00444   smtp_parms += " -- ";
00445   for (int i = 0; i < _msg_recipients->length(); i++) {
00446     istring name;
00447     istring just_addr = prune_address(_msg_recipients->get(i), name);
00448     smtp_parms += just_addr;
00449     smtp_parms += " ";
00450   }
00451 
00452   for (int i = 0; i < _msg_carbons->length(); i++) {
00453     istring name;
00454     istring just_addr = prune_address(_msg_carbons->get(i), name);
00455     smtp_parms += just_addr;
00456     smtp_parms += " ";
00457   }
00458 
00459 //LOG(istring("about to run: ") + msmtp_path);
00460 //LOG(istring("with parms: ") + smtp_parms);
00461 
00462   stdio_redirecter msmtp(msmtp_path, smtp_parms);
00463   if (msmtp.health() != stdio_redirecter::OKAY) {
00464     // that didn't work.
00465     LOG("failure to launch the msmtp application with redirected I/O.");
00466     return BAD_INPUT;  // we're calling this a configuration problem.
00467   }
00468   if (!msmtp.running()) {
00469     // somehow it launched but exited already?
00470     LOG("the msmtp application has unexpectedly left the process list.");
00471     return BAD_INPUT;  // we're calling this a configuration problem.
00472   }
00473 
00474   if (authenticating) {
00475     // expect the first thing it says to be a password prompt.
00476 
00477 // not for windows, since we do the temporary config file instead.
00478 // the windows version of msmtp will not allow us to feed the password over
00479 // standard out since it puts the app into some kind of console only mode
00480 // where nothing but real keypresses seem to be sufficient.
00481 #ifndef __WIN32__
00482 //hmmm: fix this to use timeout as defined in interface.
00483 const int SOME_TIMEOUT = 30 * SECOND_ms;
00484 
00485     // wait until we see the prompt or suffer a timeout.
00486     time_stamp done_waiting(SOME_TIMEOUT);
00487     byte_array received;
00488     bool gave_password = false;
00489     while (time_stamp() < done_waiting) {
00490       byte_array temp;
00491       // add any new text to our input buffer.
00492       if (get_outputs(msmtp, temp)) {
00493         received += temp;
00494         // turn the bytes into a string for checking.
00495         istring text_gotten(istring::UNTERMINATED, (char *)received.observe(),
00496             received.length());
00497         if (text_gotten.contains("password")) {
00498           // we saw our keyword.  hopefully we got everything that we
00499           // needed to before sending this.
00500           int written;
00501           msmtp.write(*_sess_password + log_base::platform_ending(), written);
00502           gave_password = true;
00503 //          LOG("wrote password to smtp app.");
00504           break;  // get out of loop.
00505         }
00506       }
00507       portable::sleep_ms(40);  // snooze a sec.
00508     }
00509     if (!gave_password) {
00510       LOG("never received password prompt for authenticated smtp.");
00511       return TIMED_OUT;  // some kind of problem with the server or config.
00512     }
00513 #endif
00514   }
00515 
00516   CHECK_STILL_RUNNING(BAD_INPUT);
00517   byte_array junk;
00518   get_outputs(msmtp, junk);
00519   CHECK_FOR_PASSWORD_PROMPT(junk);
00520 
00521   int len_written = 0;  // used in writing to the smtp app.
00522 
00523 #ifdef __WIN32__
00524   // Add some standard headers to specify how we will be encoding
00525   // the message body
00526   istring header_lines = "MIME-Version: 1.0";
00527   header_lines += log_base::platform_ending();
00528   header_lines += "Content-Type: text/plain; charset=\"iso-8859-1\"";
00529   header_lines += log_base::platform_ending();
00530   header_lines += "Content-Transfer-Encoding: quoted-printable";
00531   header_lines += log_base::platform_ending();
00532   msmtp.write(header_lines, len_written);
00533 #endif
00534 
00535   istring subject_line = "Subject: ";
00536   subject_line += *_msg_subject; 
00537   subject_line += log_base::platform_ending();
00538   msmtp.write(subject_line, len_written);
00539     // we're not super concerned about the subject being too long to write
00540     // all of it at once.  if they passed a huge multi-megabyte subject line,
00541     // then they can just reap it.
00542 //LOG(istring("wrote ") + subject_line);
00543 
00544   // create the full-fledged from address now.
00545   istring from_address = "From: ";
00546   from_address += *_msg_sender;
00547   from_address += log_base::platform_ending();
00548   msmtp.write(from_address, len_written);
00549     // not super concerned about overly long from addresses either.
00550 //LOG(istring("wrote ") + from_address);
00551 
00552   // add the nicely formatted destination addresses.
00553   for (int i = 0; i < _msg_recipients->length(); i++) {
00554     istring to_line = "To: ";
00555     to_line += _msg_recipients->get(i);
00556     to_line += log_base::platform_ending();
00557     msmtp.write(to_line, len_written);
00558 //LOG(istring("wrote ") + to_line);
00559   }
00560 
00561   // add some CC addresses also, if any exist.
00562   for (int i = 0; i < _msg_carbons->length(); i++) {
00563     istring to_line = "CC: ";
00564     to_line += _msg_carbons->get(i);
00565     to_line += log_base::platform_ending();
00566     msmtp.write(to_line, len_written);
00567 //LOG(istring("wrote ") + to_line);
00568   }
00569 
00570   // end the headers with a blank line.
00571   msmtp.write(log_base::platform_ending(), len_written);
00572 
00573   get_outputs(msmtp, junk);
00574   CHECK_FOR_PASSWORD_PROMPT(junk);
00575 
00576   int writing = _msg_body->length();
00577   int maximum_iters = MAXIMUM_WRITE_ITERATIONS;
00578     // don't languish forever if the writes aren't working.
00579   while ( (writing > 0) && (maximum_iters-- > 0) ) {
00580     CHECK_STILL_RUNNING(BAD_INPUT);
00581     msmtp.write(*_msg_body, len_written);
00582     writing -= len_written;
00583     // prune the string if we need to try some more to write it all.
00584     if (writing)
00585       _msg_body->zap(0, len_written - 1);
00586   }
00587   msmtp.write(log_base::platform_ending(), len_written);
00588 //LOG(istring("wrote body: ") + *_msg_body);
00589 
00590 //hmmm: eventually--encode attachments and dump them in also.
00591 
00592   get_outputs(msmtp, junk);
00593   CHECK_FOR_PASSWORD_PROMPT(junk);
00594 
00595   msmtp.close_input();
00596   time_stamp when_to_leave(MAXIMUM_PAUSE_FOR_SENDING);
00597   while ( (time_stamp() < when_to_leave) && msmtp.running() ) {
00598     portable::sleep_ms(42);  // pause a bit.
00599   }
00600   if (time_stamp() > when_to_leave) return TIMED_OUT;
00601 
00602   get_outputs(msmtp, junk);
00603     // last check for pending i/o.
00604 
00605   // check the exit value to make sure things look okay.
00606   if (!okay_exit_value(msmtp.exit_value())) return BAD_INPUT;
00607 
00608   return OKAY;
00609 }
00610 
00611 
00612 #endif //SMTP_CLIENT_IMPLEMENTATION_FILE
00613 

Generated on Fri Nov 21 04:29:15 2008 for HOOPLE Libraries by  doxygen 1.5.1