=head1 NAME Synapse::MailSender - email notification system =head1 About Synapse's Open Telephony Toolkit L is a part of Synapse's Wholesale Open Telephony Toolkit. As we are refactoring our codebase, we at Synapse have decided to release a substantial portion of our code under the GPL. We hope that as a developer, you will find it as useful as we have found open source and CPAN to be of use to us. =head1 What is L all about Wether it's to send rate notifications, QOS alarms, rate import confirmation, or credit limit warnings, doing wholesale telecom requires sending mail. Lots of mail, in fact. The goal of this module is to provide a simple and easy to work with email sending library as well as a templating library. It is based on L to construct the mail and send it, and on L and L to provide an XML email templating framework. This modules allows you to constructs emails which have: =over 4 =item - an optional 'SetSender' attribute (set to 'From' by default) =item - a 'From' field =item - a 'To' field =item - one or more optional 'Cc' (carbon copy) fields =item - one or more optional 'Bcc' (blind carbon copy) fields =item - a subject field =item - One or more paragraphs, which will make up for the email contents, which is ALWAYS pure text. This module is designed for boring and dull email notifications. =item - One or more optional file attachments, since it is useful for doing things like attaching PDF invoices, Excel spreadsheets with statistics, or cdr files in .CSV format for instance. =back =head1 A simple example: Say you have template.xml: From To Subject Hello, World And somedata.yaml --- From: foo@bar.net To: baz@buz.com Subject: foo bar baz buz You can use the provided script synapse-mailsender and type in the following command to have your email sent out: synapse-mailsender ./template.xml ./somedata.yaml =head1 API =cut package Synapse::MailSender; use MIME::Lite; use MIME::Type::FileName; use XML::Parser::REX; use Petal::Tiny; use YAML::XS; use Synapse::Logger; use warnings; use strict; our $VERSION = '1.4'; =head2 $class->new(); Creates a new L object. =cut sub new { my $class = shift; my $self = bless { @_ }, $class; $self->{Sendmail} ||= '/usr/sbin/sendmail'; return $self; } =head2 $self->Sendmail ('/usr/local/bin/mysendmail'); Sets the sendmail command to use with MIME::Lite. By default, is set to /usr/sbin/sendmail. =cut sub Sendmail { my $self = shift; $self->{Sendmail} = shift; } =head2 $self->From ('from@example.com'); Sets the 'From' field. =cut sub From { my $self = shift; $self->{From} = [ @_ ]; } =head2 $self->To ('to@example.com'); Sets the 'To' field. =cut sub To { my $self = shift; $self->{To} = [ @_ ]; } =head2 $self->Cc ('cc@example.com'); Adds a carbon copy recipient. Can be invoked multiple times to add more than one carbon copy recipients. =cut sub Cc { my $self = shift; $self->{Cc} ||= []; push @{$self->{Cc}}, @_; } =head2 $self->Bcc ('cc@example.com'); Adds a 'blind carbon copy' recipient. Can be invoked multiple times to add more than one blind carbon copy recipients. =cut sub Bcc { my $self = shift; $self->{Bcc} ||= []; push @{$self->{Bcc}}, @_; } =head2 $self->Subject ('Your account is over limit'); Sets the 'Subject' field. =cut sub Subject { my $self = shift; $self->{Subject} = shift; } =head2 $self->SetSender ('Your account is over limit'); Sets the 'SetSender' field. If not set, the 'From' field will be used, which is what you want most of the time anyways. =cut sub SetSender { my $self = shift; $self->{SetSender} = shift; } =head2 $self->Body ('Dear Customer'); Adds a paragraph to the text body. i.e. you can call this method once per paragraph. =cut sub Body { my $self = shift; $self->{Body} ||= []; push @{$self->{Body}}, @_; } =head2 $self->Say ('Dear Customer'); Alias for Body(), looks nicer in templates. =cut sub Say { my $self = shift; $self->{Body} ||= []; push @{$self->{Body}}, @_; } =head2 $self->Para ('Dear Customer'); Another alias for Body(), can't make up my mind right now... =cut sub Para { my $self = shift; $self->{Body} ||= []; push @{$self->{Body}}, @_; } =head2 $self->Attach ('/path/to/file.xls'); Attaches a file to the message. =cut sub Attach { my $self = shift; $self->{Attach} ||= []; push @{$self->{Attach}}, @_; } sub None {} =head2 $self->loadxml ($path_to_xml_template, option1 => $option1, option2 => $option2, etc) Uses L to process $path_to_xml_template. Passes the following arguments to the template: =over 4 =item self : current L object =item anything else you pass to it, i.e. in this case 'option1' and 'option2'. =back Say your code looks like this: my $sMailSender = Synapse::MailSender->new(); $sMailSender->loadxml ( '/opt/templates/accountsuspended.xml', user => $user, accountDetailsFile => $user->accountFile() ); $sMailSender->send(); Your template itself may look roughly like this: example@example.com example@example.com example@example.com example@example.com Your account is over limit Dear Customer, Unfortunately, your account with a balance of 0.00 has reached its allowed limit. Your services are being suspended for now. We kindly request that you post a payment with us so that your account reaches its allowed credit limit. Get in touch. Cheers Ourselves (example@example.com) some-file.xls =head2 $self->loadxml ($path_to_xml_template, $yamlfile) Same as above, but passes a YAML file as options for template processing. The Dumped YAML is passed as 'yaml' in the template. option1 => $option1, option2 => $option2, etc) =head2 $self->loadxml ($xmldata, option1 => $option1, option2 => $option2, etc) Same as above, but instead of passing an XML Template name, the XML template data is passed directly. =head2 $self->loadxml ($xmldata, $yamlfile) Spame as above, but instead of passing an XML Template name, the XML template data is passed directly. Plus, passes a YAML file as options for template processing. The Dumped YAML is passed as 'yaml' in the template. option1 => $option1, option2 => $option2, etc) =cut sub loadxml { my $self = shift; my $xml = shift; my $tmpl = Petal::Tiny->new ($xml); my $res = (@_ == 1) ? $tmpl->process (self => $self, yaml => _loadyaml (shift())) : $tmpl->process (self => $self, @_); $self->_loadxml($res); } sub _loadyaml { my $yamlfile = shift @_; open YAML, "<$yamlfile" or do { logger ("cannot read open YAML file $yamlfile"); die "Cannot read open $yamlfile"; }; my $data = join '', ; close YAML; my $res = eval { my ($yaml) = Load $data; $yaml; }; $@ and logger($@); return $res; } sub _loadxml { my $self = shift; my $xmldata = shift; eval { my @tokens = XML::Parser::REX::ShallowParse ($xmldata); my $method = 'None'; for (@tokens) { /^\/i and do { $method = 'SetSender'; next }; /^\/i and do { $method = 'From'; next }; /^\/i and do { $method = 'To'; next }; /^\/i and do { $method = 'Cc'; next }; /^\/i and do { $method = 'Bcc'; next }; /^\/i and do { $method = 'Subject'; next }; /^\/i and do { $method = 'Body'; next }; /^\/i and do { $method = 'Say'; next }; /^\/i and do { $method = 'Para'; next }; /^\/i and do { $method = 'Attach'; next }; /^\$method($_); } }; $@ and logger($@); } =head2 $self->message(); Once you have configured your L object with the methods above, you can construct a L object by invoking $self->message(); =cut sub message { my $self = shift; my $from = ref $self->{From} ? join ', ', @{$self->{From}} : $self->{From}; my $to = ref $self->{To} ? join ', ', @{$self->{To}} : $self->{To}; my $cc = ref $self->{Cc} ? join ', ', @{$self->{Cc}} : $self->{Cc}; my $bcc = ref $self->{Bcc} ? join ', ', @{$self->{Bcc}} : $self->{Bcc}; my $subject = $self->{Subject}; my $body = join "\n\n", @{$self->{Body}}; ### Create a new multipart message: my %args = ( From => $from, To => $to, Subject => $subject, Type => 'multipart/mixed' ); $args{Cc} = $cc if ($cc); $args{Bcc} = $bcc if ($bcc); ### Add parts (each "attach" has same arguments as "new"): my $msg = MIME::Lite->new (%args); $msg->attach ( Type => 'TEXT', Data => $body ); $self->{Attach} ||= []; foreach my $file ( @{$self->{Attach}} ) { my $type = MIME::Type::FileName::guess ($file); $msg->attach ( Type => $type, Path => $file, Disposition => 'attachment' ); } ### return final object return $msg; } =head2 $self->send(); Once you have configured your L object with the methods above, you can send the corresponding email message using this method. =cut sub send { my $self = shift; my $msg = $self->message(); my $str = $msg->as_string; # somehow this seems to fix a bug where by sometimes the message is empty... $msg->send_by_sendmail ( Sendmail => $self->{Sendmail}, SetSender => $self->{SetSender} || $self->{From}->[0], ); } 1; __END__ =head1 EXPORTS none. =head1 BUGS Please report them to me. Patches always welcome... =head1 AUTHOR Jean-Michel Hiver, jhiver (at) synapse (dash) telecom (dot) com This library is free software; you can redistribute it and/or modify it under the same terms as Perl itself. =cut