1 /* Copyright (C) 2000-2006 Thomas Bopp, Thorsten Hampel, Ludger Merkens
3 * This program is free software; you can redistribute it and/or modify
4 * it under the terms of the GNU General Public License as published by
5 * the Free Software Foundation; either version 2 of the License, or
6 * (at your option) any later version.
8 * This program is distributed in the hope that it will be useful,
9 * but WITHOUT ANY WARRANTY; without even the implied warranty of
10 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 * GNU General Public License for more details.
13 * You should have received a copy of the GNU General Public License
14 * along with this program; if not, write to the Free Software
15 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
17 * $Id: smtp.pike,v 1.2 2009/05/06 19:23:10 astra Exp $
19 inherit "/kernel/secure_mapping";
20 inherit "/base/serialize";
25 #include <attributes.h>
27 //! This is the SMTP module for sTeam. It sends mail to some e-mail adress
28 //! by using a local mailserver or doing MX lookup and sending directly
29 //! to the targets mail server.
30 class smtp : public secure_mapping,serialize{
39 #define SMTP_LOG(s, args...) werror("smtp: "+s+"\n", args)
41 #define SMTP_LOG(s, args...)
44 #if constant(Protocols.SMTP.client)
45 #define SMTPCLIENT Protocols.SMTP.client
47 #define SMTPCLIENT Protocols.SMTP.Client
55 Thread.Queue MsgQueue = Thread.Queue();
56 Thread.Mutex saveMutex = Thread.Mutex();
58 object oSMTP; // cache smtp object (connection)
59 object oDNS; // cache DNS Client
61 private function myfDb;
62 private string mysDbTable;
65 private mapping test_objects = ([ ]);
70 mixed err = catch( oDNS = Protocols.DNS.client() );
72 werror( "smtp: warning, could not create DNS client.\n" );
74 add_data_storage(STORE_SMTP, retrieve_mails, restore_mails);
80 mapping retrieve_mails()
88 void restore_mails(mapping data)
99 [myfDb , mysDbTable] = _Database->connect_db_mapping();
101 if( search(myfDb()->list_tables(), "i_rawmails" ) == -1 ) {
102 myfDb()->big_query("create table i_rawmails (mailid int, rawdata longtext, UNIQUE(mailid))");
105 // load all mails, which have not been send previously
107 err = catch(load_mails());
108 if ( err ) MESSAGE("Error loading mails:\n%O\n%O", err[0], err[1]);
118 // load mails where ?
119 array mails = index();
121 foreach(mails, string id) {
123 msgId = max(i, msgId);
126 // see if something is in raw data table
127 Sql.sql_result res = myfDb()->big_query(sprintf("select rawdata from i_rawmails where mailid='%s'", id));
129 while ( row = res->fetch_row() ) {
130 #if constant(steamtools.unserialize)
131 msg->raw = steamtools.unserialize(row[0], find_object);
133 msg->raw = unserialize(row[0]);
136 if ( mappingp(msg) ) {
138 deliver_message(msg);
146 void save_mail(mapping msg)
148 // save mails where ?
151 object lock = saveMutex->lock();
159 #if constant(steamtools.serialize)
160 serializer = steamtools.serialize;
162 serializer = serialize;
164 set_value((string)msgId, m);
165 if ( stringp(msg->raw) && strlen(msg->raw)>0 ) {
166 myfDb()->big_query(sprintf("insert into i_rawmails values ('%d', '%s')",
167 msgId, myfDb()->quote(serializer(msg->raw))));
171 FATAL("Failed to save mail: %O\n%O", err[0], err[1]);
179 void delete_mail(mapping msg)
182 myfDb()->big_query(sprintf("delete from i_rawmails where mailid='%d'",
188 void runtime_install()
190 SMTP_LOG("Init module SMTP !");
192 // an initial connection needs to be created to load some libraries
193 // otherwise creating connections will fail after the sandbox
194 // is in place (chroot("server/"))
195 server = _Server->query_config(CFG_MAILSERVER);
196 port = _Server->query_config(CFG_MAILPORT);
197 if (objectp(oDNS) && stringp(server) && sizeof(server) > 0 ) {
198 array result = oDNS->gethostbyname(lower_case(server));
199 if (arrayp(result) && sizeof(result)>1 && arrayp(result[1]) && sizeof(result[1])>0) {
200 server = result[1][0];
201 MESSAGE("Using SMTP Server Adress: %O", server);
204 if ( !intp(port) || port <= 0 )
209 if ( stringp(server) && sizeof(server) > 0 && server != "disabled" )
210 err = catch( oSMTP = SMTPCLIENT( server, port ) );
212 FATAL("Failed to connect to " + server+" :\n"+sprintf("%O\n", err));
214 start_thread(smtp_thread);
217 void deliver_message(mapping msg)
219 object serviceMod = get_module("ServiceManager");
220 if ( objectp(serviceMod) && serviceMod->is_service("smtp") ) {
221 serviceMod->call_service("smtp", ([ "action": "send", "msg": msg, ]));
224 MsgQueue->write(msg);
230 send_mail(array|string email, string subject, string body, void|string from, void|string fromobj, void|string mimetype, void|string fromname, void|string date, void|string message_id, void|string in_reply_to, void|string reply_to, void|string mail_followup_to)
234 msg->subject = subject;
235 if ( stringp(subject) )
236 msg->subject = Messaging.get_quoted_string( subject );
237 msg->mimetype = (stringp(mimetype) ? mimetype : "text/plain");
238 if ( lower_case(msg->mimetype) == "text/html" )
239 msg->body = Messaging.fix_html( body );
242 msg->date = date || timelib.smtp_time(time());
243 msg->message_id = message_id||("<"+(string)time()+(fromobj||("@"+_Server->get_server_name()))+">");
246 msg->reply_to=reply_to;
248 msg->mail_followup_to=mail_followup_to;
250 msg->in_reply_to=in_reply_to;
252 get_module("log")->log_debug( "smtp", "send_mail: to=%O", email );
254 if ( stringp(from) && sizeof(from) > 0 )
257 msg->from = this_user()->get_identifier() +
258 "@" + _Server->get_server_name();
259 if ( stringp(fromobj) )
260 msg->fromobj = fromobj;
261 if ( stringp(fromname) )
262 msg->fromname = fromname;
265 deliver_message(msg);
267 object user = geteuid() || this_user();
268 if ( objectp(user) && user->is_storing_sent_mail() &&
269 objectp(user->get_sent_mail_folder()) ) {
270 object mail_copy = get_factory(CLASS_DOCUMENT)->execute(
271 ([ "name":msg->subject, "mimetype": msg->mimetype ]) );
272 mail_copy->set_attribute( OBJ_DESC, msg->subject );
274 if ( arrayp(email) ) to_arr = email;
275 else to_arr = ({ email });
276 mail_copy->set_attribute( "mailto", to_arr );
277 mail_copy->set_content( msg->body );
278 mail_copy->sanction_object( user, SANCTION_ALL );
279 mail_copy->set_acquire( 0 );
280 object old_euid = geteuid();
282 get_module( "table:read-documents" )->download_document( 0, mail_copy, UNDEFINED ); // mark as read
283 foreach ( mail_copy->get_annotations(), object ann )
284 get_module( "table:read-documents" )->download_document( 0, ann, UNDEFINED ); // mark as read
286 user->get_sent_mail_folder()->add_annotation( mail_copy );
290 void send_mail_raw(string|array email, string data, string from)
296 if ( stringp(from) && sizeof(from) > 0 )
299 msg->from = this_user()->get_identifier()
300 + "@" + _Server->get_server_name();
303 deliver_message(msg);
306 void send_mail_mime(string email, object message)
308 mapping mimes = message->query_attribute(MAIL_MIMEHEADERS);
310 sscanf(mimes->from, "%*s<%s>", from);
311 if ( !stringp(from) || sizeof(from) < 1 )
312 from = this_user()->get_identifier() + "@" + _Server->get_server_name();
313 send_mail(email, message->get_identifier(), message->get_content(), from);
317 mixed cb_tag(Parser.HTML p, string tag)
319 if ( search(tag, "<br") >= 0 || search (tag, "<BR") >= 0 )
326 void send_message(mapping msg)
330 if ( !intp(port) || port <= 0 ) port = 25;
332 object log = get_module("log");
334 log->log_debug( "smtp", "send_message: server: %O:%O", server, port );
336 if ( !stringp(msg->from) ) {
337 msg->from = "admin@"+_Server->get_server_name();
338 log->log_debug( "smtp", "send_message: invalid 'from', using %s",
341 log->log_debug( "smtp", "send_message: mail from %O to %O (server: %O:%O)\n"
342 + " Subject: %O", msg->from, msg->email, server, port, msg->subject );
344 if ( !arrayp(msg->email) )
345 msg->email = ({ msg->email });
347 foreach ( msg->email, string email ) {
348 string tmp_server = server;
351 if ( stringp(server) && sizeof(server) > 0 ) {
353 smtp = SMTPCLIENT( server, port );
357 // if no server is configured use the e-mail of the receiver
359 sscanf( email, "%*s@%s", host );
360 if ( !stringp(host) )
361 steam_error("MX Lookup failed, host = 0 in %O", msg->email);
363 if ( !objectp(oDNS) )
364 steam_error("MX Lookup failed, no DNS");
365 tmp_server = oDNS->get_primary_mx(host);
366 if ( !stringp(tmp_server) )
368 array dns_data = oDNS->gethostbyname(tmp_server);
369 if ( arrayp(dns_data) && sizeof(dns_data) > 1 &&
370 arrayp(dns_data[1]) && sizeof(dns_data[1]) > 0 )
371 tmp_server = dns_data[1][0];
373 log->log_debug( "smtp", "send_message: MX lookup: %O:%O",
374 tmp_server, tmp_port );
376 smtp = SMTPCLIENT( tmp_server, tmp_port );
380 if ( !objectp(smtp) || smtp_error ) {
381 string msg = sprintf( "Invalid mail server %O:%O (from %O to %O)\n",
382 tmp_server, tmp_port, msg->from, email );
383 if ( smtp_error ) msg += sprintf( "%O", smtp_error );
384 log->log_error( "smtp", msg );
388 if ( stringp(msg->rawmime) ) {
391 smtp->send_message(msg->from, ({ email }), msg->rawmime);
394 log->log_error( "smtp",
395 "Failed to send mail directly from %O to %O via %O:%O : %O",
396 msg->from, email, tmp_server, tmp_port, smtp_error[0]);
399 log->log_info( "smtp", "Mail sent directly from %O to %O via %O:%O\n",
400 msg->from, email, tmp_server, tmp_port );
405 if ( !stringp(msg->mimetype) )
406 msg->mimetype = "text/plain";
408 if ( !stringp(msg->body) )
409 log->log_error( "smtp", "Invalid message body from %O to %O:\n%O",
410 msg->from, email, msg->body );
412 MIME.Message mmsg = MIME.Message(
414 ([ "Content-Type": (msg->mimetype||"text/plain") + "; charset=utf-8",
415 "Mime-Version": "1.0 (generated by open-sTeam)",
416 "Subject": msg->subject||"",
417 "Date": msg->date || timelib.smtp_time(time()),
418 "From": msg->fromname||msg->from||msg->fromobj||"",
419 "To": (msg->fromobj ? msg->fromobj : email)||"",
420 "Message-Id": msg->message_id||"",
423 if(msg->mail_followup_to)
424 mmsg->headers["Mail-Followup-To"]=msg->mail_followup_to;
426 mmsg->headers["Reply-To"]=msg->reply_to;
428 mmsg->headers["In-Reply-To"]=msg->in_reply_to;
431 smtp->send_message(msg->from, ({ email }), (string)mmsg);
434 log->log_error( "smtp", "Failed to send mail from %O to %O via %O:%O"
435 + " : %O\n", msg->from, email, tmp_server, tmp_port, smtp_error[0] );
438 log->log_info( "smtp", "Mail sent from %O to %O via %O:%O\n",
439 msg->from, email, tmp_server, tmp_port );
450 SMTP_LOG("smtp-thread running...");
451 msg = MsgQueue->read();
455 get_module("log")->log_debug( "smtp",
456 "Message from %O to %O sent: '%O'", msg->from, msg->email,
457 (stringp(msg->rawmime)?"mime message": msg->subject) );
460 FATAL("Error while sending message: " + err[0] +
461 sprintf("\n%O\n", err[1]));
462 if ( server == "disabled" ) {
463 sleep(600); // wait 10 minutes (config could change) and continue
467 FATAL("MAILSERVER="+_Server->query_config(CFG_MAILSERVER));
468 if ( objectp(oSMTP) ) {
472 sleep(60); // wait one minute before retrying
479 string get_identifier() { return "smtp"; }
480 string get_table_name() { return "smtp"; }
484 void test( void|int try_nr )
486 object services = get_module("ServiceManager");
487 if ( !objectp(services) || !services->is_service("smtp") && try_nr < 12 ) {
488 Test.add_test_function( test, 10, try_nr+1 );
491 Test.test( "have smtp service",
492 objectp(services) && services->is_service("smtp") );
493 // check the send mail functionality using different ways
494 // due to the behaviour of the message system the mails are sent and
495 // later checked to see whether the have been received within the server
497 // use configured email(s)
498 string email = _Server->query_config("email");
499 if ( !stringp(server) || strlen(server) == 0 ) {
500 Test.skipped( "smtp", "no mailserver set" );
504 object old_euid = geteuid();
505 seteuid( USER("root") );
507 // create temporary test users and group:
509 int tmp_name_count = 1;
512 tmp_name = "mailtest_sender_" + ((string)time()) + "_" +
513 ((string)tmp_name_count++);
514 user_sender = USER( tmp_name );
515 } while ( objectp(user_sender) );
516 user_sender = get_factory(CLASS_USER)->execute( ([ "name": tmp_name,
517 "pw":"test", "email": GROUP("admin")->get_steam_email(), ]) );
518 if ( objectp(user_sender) )
519 test_objects["sender"] = user_sender;
521 object user_receiver;
524 tmp_name = "mailtest_receiver_" + ((string)time()) + "_" +
525 ((string)tmp_name_count++);
526 user_receiver = USER( tmp_name );
527 } while ( objectp(user_receiver) );
528 user_receiver = get_factory(CLASS_USER)->execute( ([ "name": tmp_name,
530 if ( objectp(user_receiver) )
531 test_objects["receiver"] = user_receiver;
536 tmp_name = "mailtest_group_" + ((string)time()) + "_" +
537 ((string)tmp_name_count++);
538 group = GROUP( tmp_name );
539 } while ( objectp(group) );
540 group = get_factory(CLASS_GROUP)->execute( ([ "name": tmp_name, ]) );
541 if ( objectp(group) )
542 test_objects["group"] = group;
544 // setup temporary users and group:
545 object sent_mail = user_sender->create_sent_mail_folder();
546 user_sender->set_is_storing_sent_mail( true );
547 user_receiver->set_attribute( USER_FORWARD_MSG, 1 );
548 if ( arrayp(_Server->get_cmdline_email_addresses()) ) {
549 if ( sizeof(_Server->get_cmdline_email_addresses()) > 0 )
550 user_receiver->set_attribute( USER_EMAIL,
551 _Server->get_cmdline_email_addresses()[0] );
552 foreach ( _Server->get_cmdline_email_addresses(), string email ) {
553 get_module("forward")->add_forward( user_receiver, email );
556 group->add_member( user_receiver );
558 // switch to sender uid and prepare tests:
559 seteuid( user_sender );
560 mapping sent = ([ ]);
561 mapping receive = ([ ]);
564 // test sending to users:
565 object testmail1 = user_receiver->mail( "test user mail (1)",
566 "test mailsystem 1" );
567 Test.test( "sending direct user mail", objectp(testmail1) );
568 if ( objectp(testmail1) )
569 sent[ "storing direct user mail in sent-mail" ] = "test mailsystem 1";
571 Test.skipped( "storing direct user mail in sent-mail" );
573 object factory = get_factory(CLASS_DOCUMENT);
574 object plaintext = factory->execute( (["name":"test2.txt" ]) );
575 plaintext->set_content("mail mit object an user (2)");
576 object testmail2 = user_receiver->mail(plaintext, "test mailsystem 2");
577 Test.test( "sending mail document", objectp(testmail2) );
578 if ( objectp(testmail2) )
579 sent[ "storing mail document in sent-mail" ] = "test2.txt";
581 Test.skipped( "storing mail document in sent-mail" );
583 object html = factory->execute( (["name":"test3.html", ]) );
584 html->set_content("<html><body><h2>Testing mail function with HTML body! (3)</h2></body></html>");
585 object testmail3 = user_receiver->mail(html, "test mailsystem 3");
586 Test.test( "sending mail with html document", objectp(testmail3) );
587 if ( objectp(testmail3) )
588 sent[ "storing mail with html document in sent-mail" ] = "test3.html";
590 Test.skipped( "storing mail with html document in sent-mail" );
592 object ann = factory->execute( (["name":"test4.html", ]) );
593 plaintext = factory->execute( (["name":"test4.txt" ]) );
594 plaintext->set_content("mail with object and attachement (4)");
595 ann->set_content("<html><body><h2>Testing mail function as HTML annotation!</h2></body></html>");
596 plaintext->add_annotation(ann);
597 object testmail4 = user_receiver->mail(plaintext, "test mailsystem 4");
598 Test.test( "sending mail with annotated document", objectp(testmail4) );
599 if ( objectp(testmail4) )
600 sent[ "storing mail with annotated document in sent-mail" ] = "test4.txt";
602 Test.skipped( "storing mail with annotated document in sent-mail" );
604 MESSAGE("Testing direct sending with mail system to %O",
605 user_receiver->get_steam_email());
606 testname = "testmail-5 to user address " + ((string)time());
607 send_mail( user_receiver->get_steam_email(), testname,
608 "direct test (5)", "admin@steam.uni-paderborn.de" );
609 receive[ "sending directly to user email address" ] = testname;
610 testname = "testmail-6 to user addresses (array) " + ((string)time());
611 send_mail( ({ user_receiver->get_steam_email(),
612 user_receiver->get_steam_email() }), testname,
613 "test with an array of recipients (6)",
614 "admin@steam.uni-paderborn.de" );
615 receive[ "sending directly to user email addresses (array)" ] = testname;
617 plaintext = factory->execute( (["name":"mailtest-7.txt" ]) );
618 plaintext->set_content("Testing mail with PDF attachement (0 byte) (7)");
619 object pdf = factory->execute( ([ "name": "test.pdf" ]) );
620 pdf->set_content("");
621 plaintext->add_annotation( pdf );
622 object testmail7 = user_receiver->mail( plaintext );
623 Test.test( "sending mail with empty pdf attachment", objectp(testmail7) );
624 if ( objectp(testmail7) )
625 sent[ "storing mail with empty pdf attachment in sent-mail" ] = "mailtest-7.txt";
627 Test.skipped( "storing mail with empty pdf attachment in sent-mail" );
629 // temporary disable mail host (MX test)
630 string smtphost = _Server->query_config(CFG_MAILSERVER);
631 _Server->set_config(CFG_MAILSERVER, 0, true);
633 MESSAGE("Testing MX Lookup !");
634 testname = "testmail-8 to user address (mx)" + ((string)time());
635 send_mail( user_receiver->get_steam_email(), testname,
636 "Testing mail with MX (8)", "admin@steam.uni-paderborn.de" );
637 receive[ "sending mail to user via mx lookup" ] = testname;
639 _Server->set_config(CFG_MAILSERVER, smtphost, true);
641 // test sending to groups
642 testname = "testmail-9 to group address " + ((string)time());
643 send_mail( group->get_steam_email(), testname,
644 "Testing mail to group", "admin@steam.uni-paderborn.de" );
645 receive[ "sending mail to group via mail address" ] = testname;
647 testname = "testmail-10 to a group " + ((string)time());
648 plaintext = factory->execute( ([ "name":testname ]) );
649 plaintext->set_attribute(DOC_MIME_TYPE, "text/plain");
650 plaintext->set_content("Mail to group (10) ");
651 group->mail( plaintext );
652 receive[ "sending mail as plaintext to group" ] = testname;
653 sent[ "storing mail as plaintext to group in sent-mail" ] = testname;
657 Test.add_test_function( test_more, 10, sent, receive );
660 void test_more ( mapping sent, mapping receive, void|int nr_try ) {
661 array received = test_objects["receiver"]->get_annotations();
662 foreach ( indices(receive), string msg ) {
664 string name = receive[msg];
665 foreach ( received, object obj ) {
666 if ( obj->get_identifier() == name ||
667 obj->query_attribute(OBJ_DESC) == name ) {
672 if ( objectp(found) ) {
673 m_delete( receive, msg );
674 Test.succeeded( msg );
678 array sent_mails = test_objects["sender"]->get_sent_mail_folder()->get_annotations();
679 foreach ( indices(sent), string msg ) {
681 string name = sent[msg];
682 foreach ( sent_mails, object obj ) {
683 if ( obj->get_identifier() == name ||
684 obj->query_attribute(OBJ_DESC) == name ) {
689 if ( objectp(found) ) {
690 m_delete( sent, msg );
691 Test.succeeded( msg );
695 if ( sizeof(receive) > 0 && sizeof(sent) > 0 ) {
696 if ( nr_try < 12 ) Test.add_test_function( test_more, 10, sent, receive, nr_try+1 );
698 foreach ( indices(receive), string msg )
700 foreach ( indices(sent), string msg )
705 Test.add_test_function( test_wait_for_empty_mail_queue, 10, 0 );
708 void test_wait_for_empty_mail_queue ( void|int try_nr ) {
709 if ( MsgQueue->size() > 0 && try_nr < 18 ) {
710 Test.add_test_function( test_wait_for_empty_mail_queue, 10, try_nr+1 );
713 // This doesn't seem to work, we might need to wait for the MsgQueue to be
714 // empty for a longer time or something, don't know...
715 //Test.test( "\"send\" mail queue empty", MsgQueue->size() < 1 );
716 Test.skipped( "\"send\" mail queue empty (test currently broken)" );
719 void test_cleanup () {
720 if ( mappingp(test_objects) ) {
721 object old_euid = geteuid();
722 seteuid( USER("root") );
723 foreach ( values(test_objects), object obj ) {
724 catch( obj->delete() );