send html email w/inline images

If you've made a patch to quick fix a bug or to add a new feature not yet in the main tree then post it here so others can try it out.
carteriii
Posts: 65
Joined: Sun Oct 28, 2007 3:13 pm

send html email w/inline images

Post by carteriii »

I searched the forums and didn't find a clean way to send inline images with html emails, but in playing around I found it was amazingly easy to do. I think this approach might be worthy of being put in a future release.

My approach requires using MIME::Lite to send an email (via the ZM_NEW_MAIL_MODULES selection).

I'm going to start by putting the modified zmfilter.pl code here, because I think it's easiest to explain in reverse order of how it is coded.

Code: Select all

        if ( ZM_NEW_MAIL_MODULES )
        {
            ### Create the multipart container
            my $mail = MIME::Lite->new (
                From => ZM_FROM_EMAIL,
                To => ZM_EMAIL_ADDRESS,
                Subject => $subject,
                Type => "multipart/mixed"
            );

            ### Create an image tag for each attachment
            my $attachment_count = 0;
            my $img_tags = "";
            foreach my $attachment ( @attachments )
            {
               $attachment_count++;
               $img_tags .= qq {<img src="cid:inline_image$attachment_count"><br>};
            }

            ### Add an html message part
            $mail->attach (
                Type => "text/html",
                Data => qq {<body>$img_tags<br>$body</body>}
            );

            ### Add the text message part
            $mail->attach (
                Type => "TEXT",
                Data => $body
            );
            ### Add the attachments
            ### Add the counter to create an image Id
            ### Note that is must be declared "my" or else it will
            ### be considered global and cause the entire zmfilter.pl to fail
            $attachment_count = 0;
            foreach my $attachment ( @attachments )
            {
                Info( "Attaching '$attachment->{path}\n" );
                $mail->attach(
                    Path => $attachment->{path},
                    Type => $attachment->{type},
                    Id => ('inline_image' . ++$attachment_count),
                    Disposition => "attachment"
                );
            }
            ### Send the Message
            MIME::Lite->send( "smtp", ZM_EMAIL_HOST, AuthUser=>'you@yourdomain.com', AuthPass=>'yourpass', Timeout=>60 );
            $mail->send();
        } 
The first thing to notice is that I assigned an Id to the image and named it "inline_image" plus a counter, so inline_image1, inline_image2, etc.

That ID can be referenced by an html image tag, using the CID identifier. You'll note that earlier in the code I loop over all the attachments and build an image tag for each image. Immediately after that, I just add an html message part. Note that I put my $img_tags and the standard $body with a <body> tag section.

I then went to the options screen of ZM and simply added <br> to the end of each line such that they would appear correctly in an html message. If the message is plain text, I see the extra "<br>" at the end of each line, but I can deal with that. In theory I could remove the plain text message part, but it doesn't hurt to keep and just shows up as a text attachment when the email client views the html. For clients that don't show html messages, you still get the plain text. Seems like the best of both worlds for minimal overhead.

I don't believe this addition of an Id causes any problems with plain text emails and attachments. Consequently it's just the creation of the image tags and the html message part that could be wrapped in an "if" statement which executes dependent upon a new configuration option (much like the ZM_NEW_EMAIL_MODULES option).

Finally note that I'm using the AuthUser and AuthPass to authenticate against my SMTP server. I posted that a while back, and I still believe those fields ought to become standard in the email options screen.
richbl
Posts: 8
Joined: Sun Aug 03, 2008 4:37 am

Post by richbl »

Interesting idea.

For those who want to implement this correctly, the code that's mentioned here replaces the code found in function SendEmail() of file zmfilter.pl (found in /usr/bin).

Also, two questions for the author:

--You iterate through attachments, but I'm unclear as to what attachments you're looking at when you do this. Can you please clarify?

--Are there any ZoneMinder settings that are assumed for this functionality to work? I am aware of the need to enable ZM_NEW_MAIL_MODULES.


So far, I've been unable to get this code to do as advertised. The resulting email contains no <img> tags (hence the reason for my first question).

thanks much
carteriii
Posts: 65
Joined: Sun Oct 28, 2007 3:13 pm

Post by carteriii »

Good questions. I tried to be completely, but obviously missed a few things.

Zoneminder already supports the use of %EI1% and %EIM% in the email message which will generate attachment for the first image that matches the alarmed event or the one with the highest score. You need to add one or both of those (I just use %EIM%) to your ZM_EMAIL_BODY in the options screen. The existing Zoneminder code will then gather those attachments which I loop over in that code.

I think that's it. Sorry I forgot to mention that before.
richbl
Posts: 8
Joined: Sun Aug 03, 2008 4:37 am

Post by richbl »

Ahhh... that was the missing item, the requirement for %EI1%, %EIM%, (or presumably %EIV%).

Seems to work just fine now.

Also, regarding your comment on using SMTP authentication as a default, while I agree this is useful, the final implementation needs be optional. In my case--and likely the case of more than just myself--I have a mail server running that handles SMTP authentication. So, in your code fragment here in this thread, I replaced your line with the authentication-free version:

from:
MIME::Lite->send( "smtp", ZM_EMAIL_HOST, AuthUser=>'EMAIL@ADDR.COM', AuthPass=>'XXX', Timeout=>60 );

to:
MIME::Lite->send( "smtp", ZM_EMAIL_HOST, Timeout=>60 );

as my ZM_EMAIL_HOST is local, and hence, have no need for authentication. Again, that is handled by my mail server elsewhere on the LAN.

Thanks.
carteriii
Posts: 65
Joined: Sun Oct 28, 2007 3:13 pm

Post by carteriii »

Good catch. I'm glad it worked.
richbl
Posts: 8
Joined: Sun Aug 03, 2008 4:37 am

Post by richbl »

I've discovered a few more issues for anyone interested in pursuing this strategy of including images in an email notification:

A) The MIME content type should be changes from "multipart/mixed" to "multipart/alternative." Otherwise, the current codebase will generate two sets of messages, one plain/text and then one text/html. I'm guessing this is not intentional.

B) For correct ordering of MIME parts (assuming multipart/alternative), the plain/text type should precede the text/html content type (currently, they are reversed). This will result in the end user's mail program correctly selecting the most appropriate content type (rather than immediately selecting the plain/text content type).

C) A text/html content type should minimally include <html><body></body></html> tags. Arguably, <head></head> tags should also be included within the <html> tags. Not including these tags runs the risk of some mail programs incorrectly interpreting the html markup.

For more details on MIME (RFC 1521), see http://www.freesoft.org/CIE/RFC/1521/18.htm.

rich
dhorth
Posts: 14
Joined: Wed Sep 12, 2007 8:27 pm

Post by dhorth »

Sorry for the dumb question, but I have searched everywhere and I just can't find a simple example on how to setup a filter to email me when a motion event occurs. I have Mocord setup and I'm seeing the events. I also created a simple perl script to test the email sending function and it works. The problem seems to be that I need a filter to trigger the email, looking at the filter dialog on the home page I have no clue on how to configure it to send email. Thanks
richbl
Posts: 8
Joined: Sun Aug 03, 2008 4:37 am

Post by richbl »

dhorth wrote:Sorry for the dumb question, but I have searched everywhere and I just can't find a simple example on how to setup a filter to email me when a motion event occurs. I have Mocord setup and I'm seeing the events. I also created a simple perl script to test the email sending function and it works. The problem seems to be that I need a filter to trigger the email, looking at the filter dialog on the home page I have no clue on how to configure it to send email. Thanks
In my own case, I created a filter (from the console interface, choose Filters) called email that shows a "total score" of > than 60, with the "email details of all matches" box checked.

So, when a motion event is generated, an email gets generated. Note, however, that in my case, it typically takes 30 - 60 seconds before the filter sends off the email.

rich
dhorth
Posts: 14
Joined: Wed Sep 12, 2007 8:27 pm

Post by dhorth »

OK, now it makes more sense. Thank you. I'm now getting email successfully. However, the image tag is blank. You mention the need for
Ahhh... that was the missing item, the requirement for %EI1%, %EIM%, (or presumably %EIV%).
Where do those tags go?
richbl
Posts: 8
Joined: Sun Aug 03, 2008 4:37 am

Post by richbl »

dhorth wrote:OK, now it makes more sense. Thank you. I'm now getting email successfully. However, the image tag is blank. You mention the need for
Ahhh... that was the missing item, the requirement for %EI1%, %EIM%, (or presumably %EIV%).
Where do those tags go?
Sorry for the late response (I thought I had notifications on for this thread).

To answer your question, here's what I have in my options/email tab (for the email body, ZM_EMAIL_BODY).

Code: Select all

%EIM%
<b>WARNING!</b><br><br>
An alarm has been detected on your installation of ZoneMinder.<br><br>

The alarm occurred at <b>%MN%</b> at %ET%.<br><br>

The cause was %EC%, with a maximum event score of %ESM%.<br><br>

Currently, there are %MET% total events logged at %MN%.<br><br>
Recall that I'm formatting my emails in HTML (with a fallback to ASCII/Text), so you'll see some HTML formatting as part of that syntax.
richbl
Posts: 8
Joined: Sun Aug 03, 2008 4:37 am

Post by richbl »

For completeness, I'm posting the entirety of my modified SendEmail() function call discussed in this thread (originally posted by carteriii).

I managed to destroy a drive repartition (power-outage at exactly the wrong moment), and lost my original rewrite from this thread.

So... best that I document what I have here, just in case...

Code: Select all

sub sendEmail
{
    my $filter = shift;
    my $event = shift;

    if ( !ZM_FROM_EMAIL )
    {
        warn( "No 'from' email address defined, not sending email" );
        return( 0 );
    }
    if ( !ZM_EMAIL_ADDRESS )
    {
        warn( "No email address defined, not sending email" );
        return( 0 );
    }

    Info( "Creating notification email\n" );

    my $subject = substituteTags( ZM_EMAIL_SUBJECT, $filter, $event );
    return( 0 ) if ( !$subject );
    my @attachments;
    my $body = substituteTags( ZM_EMAIL_BODY, $filter, $event, \@attachments );
    return( 0 ) if ( !$body );

    Info( "Sending notification email '$subject'\n" );

    eval
    {
        if ( ZM_NEW_MAIL_MODULES )
        {
            ### Create the multipart container
            my $mail = MIME::Lite->new (
                From => ZM_FROM_EMAIL,
                To => ZM_EMAIL_ADDRESS,
                Subject => $subject,
                Type => "multipart/alternative"
            );

            ### Create an image tag for each attachment
            my $attachment_count = 0;
            my $img_tags = "";
            foreach my $attachment ( @attachments )
            {
               $attachment_count++;
               $img_tags .= qq {<img><br>};
            }

            ### Add the text message part
            $mail->attach (
                Type => "TEXT",
                Data => $body
            );

            ### Add an html message part
            $mail->attach (
                Type => "text/html",
                Data => qq {<html><head></head><body>$img_tags<br>$body</body></html>}
            );

            ### Add the attachments
            ### Add the counter to create an image Id
            ### Note that is must be declared "my" or else it will
            ### be considered global and cause the entire zmfilter.pl to fail
            $attachment_count = 0;
            foreach my $attachment ( @attachments )
            {
                Info( "Attaching '$attachment->{path}\n" );
                $mail->attach(
                    Path => $attachment->{path},
                    Type => $attachment->{type},
                    Id => ('inline_image' . ++$attachment_count),
                    Disposition => "attachment"
                );
            }
            ### Send the Message
            MIME::Lite->send( "smtp", ZM_EMAIL_HOST, Timeout=>60 );
            $mail->send();
        }
        else
        {
            my $mail = MIME::Entity->build(
                From => ZM_FROM_EMAIL,
                To => ZM_EMAIL_ADDRESS,
                Subject => $subject,
                Type => (($body=~/<html>/)?'text/html':'text/plain'),
                Data => $body
            );

            foreach my $attachment ( @attachments )
            {
                Info( "Attaching '$attachment->{path}\n" );
                $mail->attach(
                    Path => $attachment->{path},
                    Type => $attachment->{type},
                    Encoding => "base64"
                );
            }
            $mail->smtpsend( Host => ZM_EMAIL_HOST, MailFrom => ZM_FROM_EMAIL );
        }
    };
    if ( $@ )
    {
        warn( "Can't send email: $@" );
        return( 0 );
    }
    else
    {
        Info( "Notification email sent\n" );
    }
    my $sql = "update Events set Emailed = 1 where Id = ?";
    my $sth = $dbh->prepare_cached( $sql ) or Fatal( "Can't prepare '$sql': ".$dbh->errstr() );
    my $res = $sth->execute( $event->{Id} ) or Fatal( "Can't execute '$sql': ".$sth->errstr() );

    return( 1 );
}
As discussed previously, this function call exists in zmfilter.pl, found in /usr/bin.

Note that, if you plan to use this modified function, as currently implemented, you will only see the results of the changes when ZM_NEW_MAIL_MODULES is enabled (via Options/Email).
Voltage54
Posts: 23
Joined: Sat Feb 21, 2009 1:35 am

Post by Voltage54 »

I have a few dramas with the script, basically I can't get the inline image to show up, it just shows a 'broken image' box above the zoneminder text. As a temporary workaround, I modified the script a little bit (you can laugh, I've never written a program before so I had no idea what I was doing) and the end result was the email spits out a plain/text message with an attached picture (not embedded inline).

script as follows:

Code: Select all

sub sendEmail
{
    my $filter = shift;
    my $event = shift;

    if ( !ZM_FROM_EMAIL )
    {
        warn( "No 'from' email address defined, not sending email" );
        return( 0 );
    }
    if ( !ZM_EMAIL_ADDRESS )
    {
        warn( "No email address defined, not sending email" );
        return( 0 );
    }

    Info( "Creating notification email\n" );

    my $subject = substituteTags( ZM_EMAIL_SUBJECT, $filter, $event );
    return( 0 ) if ( !$subject );
    my @attachments;
    my $body = substituteTags( ZM_EMAIL_BODY, $filter, $event, \@attachments );
    return( 0 ) if ( !$body );

    Info( "Sending notification email '$subject'\n" );

    eval
    {
        if ( ZM_NEW_MAIL_MODULES )
        {
            ### Create the multipart container
            my $mail = MIME::Lite->new (
                From => ZM_FROM_EMAIL,
                To => ZM_EMAIL_ADDRESS,
                Subject => $subject,
                Type => "multipart/mixed"
            );

            ### Create an image tag for each attachment
            my $attachment_count = 0;
            my $img_tags = "";
            foreach my $attachment ( @attachments )
            {
               $attachment_count++;
               $img_tags .= qq {<img>};
            }

            ### Add the text message part
            $mail->attach (
                Type => "TEXT",
                Data => $body
            );

            ### Add the attachments
            ### Add the counter to create an image Id
            ### Note that is must be declared "my" or else it will
            ### be considered global and cause the entire zmfilter.pl to fail
            $attachment_count = 0;
            foreach my $attachment ( @attachments )
            {
                Info( "Attaching '$attachment->{path}\n" );
                $mail->attach(
                    Path => $attachment->{path},
                    Type => $attachment->{type},
                    Id => ('inline_image' . ++$attachment_count),
                    Disposition => "attachment"
                );
            }
            ### Send the Message
            MIME::Lite->send( "smtp", ZM_EMAIL_HOST, Timeout=>60 );
            $mail->send();
        }
        else
        {
            my $mail = MIME::Entity->build(
                From => ZM_FROM_EMAIL,
                To => ZM_EMAIL_ADDRESS,
                Subject => $subject,
                Type => (($body=~/<html>/)?'text/html':'text/plain'),
                Data => $body
            );

            foreach my $attachment ( @attachments )
            {
                Info( "Attaching '$attachment->{path}\n" );
                $mail->attach(
                    Path => $attachment->{path},
                    Type => $attachment->{type},
                    Encoding => "base64"
                );
            }
            $mail->smtpsend( Host => ZM_EMAIL_HOST, MailFrom => ZM_FROM_EMAIL );
        }
    };
    if ( $@ )
    {
        warn( "Can't send email: $@" );
        return( 0 );
    }
    else
    {
        Info( "Notification email sent\n" );
    }
    my $sql = "update Events set Emailed = 1 where Id = ?";
    my $sth = $dbh->prepare_cached( $sql ) or Fatal( "Can't prepare '$sql': ".$dbh->errstr() );
    my $res = $sth->execute( $event->{Id} ) or Fatal( "Can't execute '$sql': ".$sth->errstr() );

    return( 1 );
}


I would really like to know what I'm doing wrong though with respect to showing the image 'inline' in the html email. I have tried a few different computers and email clients now and they all yeild the same result! Any help would be greatly appreciated!
muckleroy
Posts: 8
Joined: Wed Nov 26, 2008 3:34 pm

Post by muckleroy »

I'm not getting the image(s) either.

It worked fine with the attachments, then I pasted the new sendEmail routine into zmfilter.pl and I started receiving HTML e-mails, with a image place holder, but no image. If I select the image place holder and go into properties it does not show a image path and name, it is blank.

I am using this as well...

%EIM%
<b>WARNING!</b><br><br>
An alarm has been detected on your installation of ZoneMinder.<br><br>

The alarm occurred at <b>%MN%</b> at %ET%.<br><br>

The cause was %EC%, with a maximum event score of %ESM%.<br><br>

Currently, there are %MET% total events logged at %MN%.<br><br>

Here is the source from the e-mail that was sent after an alarm event...

<html><head></head><body><img><br><img><br><br> <b>WARNING!</b><br><br>
An alarm has been detected on your installation of ZoneMinder.<br><br>
br>

The alarm occurred at <b>Backyard-South</b> at 2009-02-25 14:18:25.<br><br>

The cause was Motion, with a maximum event score of 11.<br><br>

Currently, there are 676 total events logged at Backyard-South.<br><br></body></html>
muckleroy
Posts: 8
Joined: Wed Nov 26, 2008 3:34 pm

Post by muckleroy »

Ok the problem is that the <img> in the PERL source code is being interperted as a format command by the forum software. You ahve to disable BBCode in this post so it shows all the text.

I just figured this out when I tried to post my fix. If the original poster could repost his code I think it might be better written than my kludge below. Even though a kludge, it does work. ;o)

sub sendEmail
{
my $filter = shift;
my $event = shift;

if ( !ZM_FROM_EMAIL )
{
warn( "No 'from' email address defined, not sending email" );
return( 0 );
}
if ( !ZM_EMAIL_ADDRESS )
{
warn( "No email address defined, not sending email" );
return( 0 );
}

Info( "Creating notification email\n" );

my $subject = substituteTags( ZM_EMAIL_SUBJECT, $filter, $event );
return( 0 ) if ( !$subject );
my @attachments;
my $body = substituteTags( ZM_EMAIL_BODY, $filter, $event, \@attachments );
return( 0 ) if ( !$body );

Info( "Sending notification email '$subject'\n" );

eval
{
if ( ZM_NEW_MAIL_MODULES )
{
### Create the multipart container
my $mail = MIME::Lite->new (
From => ZM_FROM_EMAIL,
To => ZM_EMAIL_ADDRESS,
Subject => $subject,
Type => "multipart/alternative"
);

### Create an image tag for each attachment
my $attachment_count = 0;
my $img_tags = "";
foreach my $attachment ( @attachments )
{
$attachment_count++;
$img_tags .= qq {<img src="http://www.yourdomain.com/zm/events/$at ... "/><br><br>};
}

### Add the text message part
$mail->attach (
Type => "TEXT",
Data => $body
);

### Add an html message part
$mail->attach (
Type => "text/html",
Data => qq {<html><head></head><body>$img_tags<br>$body</body></html>}
);

### Add the attachments
### Add the counter to create an image Id
### Note that is must be declared "my" or else it will
### be considered global and cause the entire zmfilter.pl to fail
$attachment_count = 0;
foreach my $attachment ( @attachments )
{
Info( "Attaching '$attachment->{path}\n" );
$mail->attach(
Path => $attachment->{path},
Type => $attachment->{type},
Id => ('inline_image' . ++$attachment_count),
Disposition => "attachment"
);
}
### Send the Message
MIME::Lite->send( "smtp", ZM_EMAIL_HOST, Timeout=>60 );
$mail->send();
}
else
{
my $mail = MIME::Entity->build(
From => ZM_FROM_EMAIL,
To => ZM_EMAIL_ADDRESS,
Subject => $subject,
Type => (($body=~/<html>/)?'text/html':'text/plain'),
Data => $body
);

foreach my $attachment ( @attachments )
{
Info( "Attaching '$attachment->{path}\n" );
$mail->attach(
Path => $attachment->{path},
Type => $attachment->{type},
Encoding => "base64"
);
}
$mail->smtpsend( Host => ZM_EMAIL_HOST, MailFrom => ZM_FROM_EMAIL );
}
};
if ( $@ )
{
warn( "Can't send email: $@" );
return( 0 );
}
else
{
Info( "Notification email sent\n" );
}
my $sql = "update Events set Emailed = 1 where Id = ?";
my $sth = $dbh->prepare_cached( $sql ) or Fatal( "Can't prepare '$sql': ".$dbh->errstr() );
my $res = $sth->execute( $event->{Id} ) or Fatal( "Can't execute '$sql': ".$sth->errstr() );

return( 1 );
}
Voltage54
Posts: 23
Joined: Sat Feb 21, 2009 1:35 am

Post by Voltage54 »

you appear to be correct! However, the new script still does not work for me :(

Using the updated script my 'view source' reports:

<html><head></head><body><img src="http://www.mydomain.com/zm/events//usr/ ... br><br><br>
<b>WARNING!</b><br><br>
An alarm has been detected on your installation of ZoneMinder.<br><br>

The alarm occurred at <b>Camera3</b> at 2009-03-07 11:44:28.<br><br>

The cause was Forced Web, with a maximum event score of 255.<br><br>

Currently, there are 3 total events logged at Camera3.<br><br> </body></html>

So it's partly the way there! Unfortunately the 'path' return in the script returns the entire linux path (i.e. /usr/share/zoneminder/events/3/73 etc) but I need it to return only /3/73/011-capture.jpg. Do you have any idea how I can make this change?

Thanks for your help so far :)
Post Reply