Source: helma/Mail.js

/*
 * Helma License Notice
 *
 * The contents of this file are subject to the Helma License
 * Version 2.0 (the "License"). You may not use this file except in
 * compliance with the License. A copy of the License is available at
 * http://adele.helma.org/download/helma/license.txt
 *
 * Copyright 1998-2007 Helma Software. All Rights Reserved.
 *
 * $RCSfile: Mail.js,v $
 * $Author$
 * $Revision$
 * $Date$
 */

/**
 * @fileoverview Fields and methods of the helma.Mail class.
 * <br /><br />
 * To use this optional module, its repository needs to be added to the 
 * application, for example by calling app.addRepository('modules/helma/Mail.js')
 */

// take care of any dependencies
app.addRepository('modules/helma/File.js');

/**
 * Define the global namespace if not existing
 */
if (!global.helma) {
    global.helma = {};
}

/**
 * Mail client enabling you to send e-mail via SMTP using Packages.javax.mail.
 * <br /><br />
 * @class This class provides functionality to sending
 * Email messages. 
 * A mail client object is created by using the helma.Mail() 
 * constructor. The mail object then can be manipulated and sent 
 * using the methods listed below.
 * <br /><br />
 * You will either need to set your mail server via the smtp
 * property in the app.properties or server.properties file
 * or pass the hostname of the mail server you want to use as a 
 * parameter to the constructor.
 * <br /><br />
 * Note: Make sure that the SMTP server itself is well-configured, 
 * so that it accepts e-mails coming from your server and does 
 * not deny relaying. Best and fastest configuration is of course 
 * if you run your own SMTP server (e.g. postfix) which might be 
 * a bit tricky to set up, however.</p>
 * 
 * @param {String} smtp as String, the hostname of the mail server
 * @constructor
 */
helma.Mail = function(host, port) {
    // Error code values for this.status
    var OK           = 0;
    var SUBJECT      = 10;
    var TEXT         = 11;
    var MIMEPART     = 12;
    var TO           = 20;
    var CC           = 21;
    var BCC          = 22;
    var FROM         = 23;
    var REPLYTO      = 24;
    var SETHEADER    = 25;
    var ADDHEADER    = 26;
    var GETHEADER    = 27;
    var REMOVEHEADER = 28;
    var SEND         = 30;
    var MAILPKG      = Packages.javax.mail;

    var self = this;
    var errStr = "Error in helma.Mail";

    var System              = java.lang.System;
    var Properties          = java.util.Properties;
    var IOException         = java.io.IOException;
    var Wrapper             = Packages.org.mozilla.javascript.Wrapper;
    var FileDataSource      = Packages.javax.activation.FileDataSource;
    var DataHandler         = Packages.javax.activation.DataHandler;

    var MimePart            = Packages.helma.util.MimePart;
    var MimePartDataSource  = Packages.helma.util.MimePartDataSource;

    var BodyPart            = MAILPKG.BodyPart;
    var Message             = MAILPKG.Message;
    var Session             = MAILPKG.Session;
    var InternetAddress     = MAILPKG.internet.InternetAddress;
    var AddressException    = MAILPKG.internet.AddressException;
    var MimeBodyPart        = MAILPKG.internet.MimeBodyPart;
    var MimeMessage         = MAILPKG.internet.MimeMessage;
    var MimeUtility         = MAILPKG.internet.MimeUtility;
    var MimeMultipart       = MAILPKG.internet.MimeMultipart;

    var buffer, multipart, multipartType = "mixed";
    var username, password;

    var setStatus = function(status) {
        if (self.status === OK) {
            self.status = status;
        }
        return;
    };

    var getStatus = function() {
        return self.status;
    };

    var addRecipient = function(addstr, name, type) {
        if (addstr.indexOf("@") < 0) {
            throw new AddressException();
        }
        if (name != null) {
            var address = new InternetAddress(addstr, 
                              MimeUtility.encodeWord(name.toString()));
        } else {
            var address = new InternetAddress(addstr);
        }
        message.addRecipient(type, address);
        return;
    };

    /**
     * Adds the content stored in this helma.Mail instance
     * to the wrapped message.
     * @private
     */
    var setContent = function() {
        if (buffer != null) {
            if (multipart != null) {
                var part = new MimeBodyPart();
                part.setContent(buffer.toString(), "text/plain");
                multipart.addBodyPart(part, 0);
                message.setContent(multipart);
            } else {
                message.setText(buffer.toString());
            }
        } else if (multipart != null) {
            message.setContent(multipart);
        } else {
            message.setText("");
        }
        return;
    };

    /**
     * Returns the text buffer of this mail message
     * @returns The text buffer of this mail message
     * @type java.lang.StringBuffer
     * @private
     */
    this.getBuffer = function() {
        return buffer;
    };

    /**
     * Returns the mime multipart object of this mail message
     * @returns The mime multipart object of this mail message
     * @type javax.mail.internet.MimeMultipart
     * @private
     */
    this.getMultipart = function() {
        return multipart;
    };

    /**
     * Sets username and password to use for SMTP authentication.
     * @param {String} uname The username to use
     * @param {String} pwd The password to use
     */
    this.setAuthentication = function(uname, pwd) {
        if (uname && pwd) {
            username = uname;
            password = pwd;
            // enable smtp authentication
            props.put("mail.smtp.auth", "true");
        }
        return;
    }

    /**
     * Returns the wrapped message
     * @returns The wrapped message
     * @type javax.mail.internet.MimeMessage
     */
    this.getMessage = function() {
        return message;
    };

    /**
     * Switches debug mode on or off.
     * @param {Boolean} debug If true debug mode is enabled
     */
    this.setDebug = function(debug) {
        session.setDebug(debug === true);
    };

    /**
     * The status of this Mail object. This equals <code>0</code> unless
     * an error occurred. See  {@link helma.Mail Mail.js} source code for a list of
     * possible error codes.
     */
    this.status = OK;
    
    /**
     * Sets the sender of an e-mail message.
     * <br /><br />
     * The first argument specifies the receipient's 
     * e-mail address. The optional second argument 
     * specifies the name of the recipient.
     *
     * @param {String} addstr as String, sender email address
     * @param {String} name as String, optional sender name
     */
    this.setFrom = function(addstr, name) {
        try {
            if (addstr.indexOf("@") < 0) {
                throw new AddressException();
            }
            if (name != null) {
                var address = new InternetAddress(addstr, 
                                  MimeUtility.encodeWord(name.toString()));
            } else {
                var address = new InternetAddress(addstr);
            }
            message.setFrom(address);
        } catch (mx) {
            app.logger.error(errStr + ".setFrom(): " + mx);
            setStatus(FROM);
        }
        return;
    };

    /**
     * Set a header in the e-mail message. If the given header is already set the previous
     * value is replaced with the new one.
     * @param name a header name
     * @param value the header value
     */
    this.setHeader = function(name, value) {
        try {
            message.addHeader(name, MimeUtility.encodeText(value));
        } catch (mx) {
            app.logger.error(errStr + ".setHeader(): " + mx);
            setStatus(SETHEADER);
        }
        return;
    }

    /**
     * Set a header in the e-mail message. If the given header is already set the previous 
     * value is replaced with the new one.
     * @param name a header name
     * @param value the header value
     */
    this.addHeader = function(name, value) {
        try {
            message.addHeader(name, MimeUtility.encodeText(value));
        } catch (mx) {
            app.logger.error(errStr + ".addHeader(): " + mx);
            setStatus(ADDHEADER);
        }
        return;
    }

    /**
     * Get all the headers for this header name.
     * Returns null  if no headers for this header name are available.
     * @param name a header name
     * @return {String[]} a string array of header values, or null
     */
    this.getHeader = function(name) {
        var value = null;
        try {
            value = message.getHeader(name);
            if (value && value.length) {
                for (var i = 0; i < value.length; i++) {
                    value[i] = MimeUtility.decodeText(value[i]);
                }
            }
        } catch (mx) {
            app.logger.error(errStr + ".getHeader(): " + mx);
            setStatus(GETHEADER);
        }
        return value;
    }

    /**
     * Remove all headers with this name. 
     * @param name the header name
     */
    this.removeHeader = function(name) {
        try {
            message.removeHeader(name);
        } catch (mx) {
            app.logger.error(errStr + ".removeHeader(): " + mx);
            setStatus(REMOVEHEADER);
        }
        return;
    }
    
    /**
     * Sets the Reply-To address of an e-mail message.
     *
     * @param {String} addstr as String, the reply-to email address
     */
    this.setReplyTo = function(addstr) {
        try {
            if (addstr.indexOf("@") < 0) {
                throw new AddressException();
            }
            var address = [new InternetAddress(addstr)];
            message.setReplyTo(address);
        } catch (mx) {
            app.logger.error(errStr + ".setReplyTo(): " + mx);
            setStatus(REPLYTO);
        }
        return;
    }

    /**
     * Sets the recipient of an e-mail message.
     * &nbsp;
     * The first argument specifies the receipient's 
     * e-mail address. The optional second argument 
     * specifies the name of the recipient.
     *
     * @param {String} addstr as String, receipients email address
     * @param {String} name as String, optional receipients name
     * @see #addTo
     */
    this.setTo = function(addstr, name) {
        try {
            addRecipient(addstr, name, Message.RecipientType.TO);
        } catch (mx) {
            app.logger.error(errStr + ".setTo(): " + mx);
            setStatus(TO);
        }
        return;
    };

    /**
     * Adds a recipient to the address list of an e-mail message.
     * <br /><br />
     * The first argument specifies the receipient's 
     * e-mail address. The optional second argument 
     * specifies the name of the recipient.
     *
     * @param {String} addstr as String, receipients email address
     * @param {String} name as String, optional receipients name
     * @see setTo
     */
    this.addTo = function(addstr, name) {
        try {
            addRecipient(addstr, name, Message.RecipientType.TO);
        } catch (mx) {
            app.logger.error(errStr + ".addTo(): " + mx);
            setStatus(TO);
        }
        return;
    }

    /**
     * Adds a recipient to the list of addresses to get a "carbon copy"
     * of an e-mail message.
     * <br /><br />
     * The first argument specifies the receipient's 
     * e-mail address. The optional second argument 
     * specifies the name of the recipient.
     *
     * @param {String} addstr as String, receipients email address
     * @param {String} name as String, optional receipients name
     */
    this.addCC = function(addstr, name) {
        try {
            addRecipient(addstr, name, Message.RecipientType.CC);
        } catch (mx) {
            app.logger.error(errStr + ".addCC(): " + mx);
            setStatus(CC);
        }
        return;
    }

    /**
     * Adds a recipient to the list of addresses to get a "blind carbon copy" of an e-mail message.
     * <br /><br />
     * The first argument specifies the receipient's 
     * e-mail address. The optional second argument 
     * specifies the name of the recipient.
     *
     * @param {String} addstr as String, receipients email address
     * @param {String} name as String, optional receipients name
     */
    this.addBCC = function(addstr, name) {
        try {
            addRecipient(addstr, name, Message.RecipientType.BCC);
        } catch (mx) {
            app.logger.error(errStr + ".addBCC(): " + mx);
            setStatus(BCC);
        }
        return;
    }

    /**
     * Sets the subject of an e-mail message.
     *
     * @param {String} subject as String, the email subject
     */
    this.setSubject = function(subject) {
        if (!subject) {
            return;
        }
        try {
            message.setSubject(MimeUtility.encodeWord(subject.toString()));
        } catch (mx) {
            app.logger.error(errStr + ".setSubject(): " + mx);
            setStatus(SUBJECT);
        }
        return;
    };

    /**
     * Sets the body text of an e-mail message.
     * 
     * @param {String} text as String, to be appended to the message body
     * @see #addText
     */
    this.setText = function(text) {
        if (text) {
            buffer = new java.lang.StringBuffer(text);
        }
        return;
    };

    /**
     * Appends a string to the body text of an e-mail message.
     *
     * @param {String} text as String, to be appended to the message body
     * @see #setText
     */
    this.addText = function(text) {
        if (buffer == null) {
            buffer = new java.lang.StringBuffer(text);
        } else {
            buffer.append(text);
        }
        return;
    };

    /**
     * Sets the MIME multiparte message subtype. The default value is
     * "mixed" for messages of type multipart/mixed. A common value
     * is "alternative" for the multipart/alternative MIME type.
     * @param {String} messageType the MIME subtype such as "mixed" or "alternative".
     * @see #getMultipartType
     * @see #addPart
     */
    this.setMultipartType = function(messageType) {
        multipartType = messageType;
        return;
    };

    /**
     * Returns the MIME multiparte message subtype. The default value is
     * "mixed" for messages of type multipart/mixed.
     * @return the MIME subtype
     * @type String
     * @see #setMultipartType
     * @see #addPart
     */
    this.getMultipartType = function(messageType) {
        return multipartType;
    };

    /**
     * Adds an attachment to an e-mail message.
     * <br /><br />
     * The attachment needs to be either a helma.util.MimePart Object retrieved
     * through the global getURL function, or a {@link helma.File} object, or a String.
     * <br /><br />
     * Use the getURL() function to retrieve a MIME object or wrap a
     * helma.File object around a file of the local file system.
     *
     * @param {fileOrMimeObjectOrString} obj File, Mime object or String to attach to the email
     * @param {String} filename optional name of the attachment
     * @param {String} contentType optional content type (only if first argument is a string)
     * @see global.getURL
     * @see helma.util.MimePart
     * @see helma.File
     */
    this.addPart = function(obj, filename, contentType) {
        try {
            if (obj == null) {
                throw new IOException(
                    errStr + ".addPart: method called with wrong number of arguments."
                );
            }
            if (multipart == null) {
                multipart = new MimeMultipart(multipartType);
            }
            if (obj instanceof Wrapper) {
                obj = obj.unwrap();
            }

            var part;
            if (obj instanceof BodyPart) {
                // we already got a body part, no need to convert it
                part = obj;
            } else {
                part = new MimeBodyPart();
                if (typeof obj == "string") {
                    part.setContent(obj.toString(), contentType || "text/plain");
                } else if (obj instanceof File || obj instanceof helma.File) {
                    var source = new FileDataSource(obj.getPath());
                    part.setDataHandler(new DataHandler(source));
                } else if (obj instanceof MimePart) {
                    var source = new MimePartDataSource(obj);
                    part.setDataHandler(new DataHandler(source));
                }
            }
            if (filename != null) {
                try {
                    part.setFileName(filename.toString());
                } catch (x) {}
            } else if (obj instanceof File || obj instanceof helma.File) {
                try {
                    part.setFileName(obj.getName());
                } catch (x) {}
            }
            multipart.addBodyPart(part);
        } catch (mx) {
            app.logger.error(errStr + ".addPart(): " + mx);
            setStatus(MIMEPART);
        }
        return;
    };

    /**
     * Saves this mail RFC 822 formatted into a file. The name of the
     * file is prefixed with "helma.Mail" followed by the current time
     * in milliseconds and a random number.
     * @param {helma.File} dir An optional directory where to save
     * this mail to. If omitted the mail will be saved in the system's
     * temp directory.
     */
    this.writeToFile = function(dir) {
        if (!dir || !dir.exists() || !dir.canWrite()) {
            dir = new java.io.File(System.getProperty("java.io.tmpdir"));
        }
        var fileName = "helma.Mail." + (new Date()).getTime() +
                       "." + Math.round(Math.random() * 1000000);
        var file = new java.io.File(dir, fileName);
        if (file.exists()) {
            file["delete"]();
        }
        try {
            setContent();
            var fos = new java.io.FileOutputStream(file);
            var os = new java.io.BufferedOutputStream(fos);
            message.writeTo(os);
            os.close();
            app.logger.info("helma.Mail.saveTo(): saved mail to " +
                            file.getAbsolutePath());
        } catch (e) {
            app.logger.error(errStr + ".saveTo(): " + e);
        }
        return;
    };

    /**
     * Returns the source of this mail as RFC 822 formatted string.
     * @returns The source of this mail as RFC 822 formatted string
     * @type String
     */
    this.getSource = function() {
        try {
            setContent();
            var buf = new java.io.ByteArrayOutputStream();
            var os = new java.io.BufferedOutputStream(buf);
            message.writeTo(os);
            os.close();
            return buf.toString();
        } catch (e) {
            app.logger.error(errStr + ".getSource(): " + e);
        }
        return null;
    };

    /**
     * Sends an e-mail message.
     * <br /><br />
     * This function sends the message using the SMTP
     * server as specified when the Mail object was
     * constructed using helma.Mail.
     * <br /><br />
     * If no smtp hostname was specified when the Mail
     * object was constructed, the smtp property in either
     * the app.properties or server.properties file needs
     * to be set in order for this to work.
     * <br /><br />
     * As a fallback, the message will then be written to
     * file using the {@link #writeToFile} method. 
     * Additionally, the location of the message files can
     * be determined by setting smtp.dir in app.properties
     * to the desired file path.
     */
    this.send = function() {
        if (this.status > OK) {
            app.logger.error(errStr + ".send(): Status is " + this.status);
            return;
        }
        if (host != null) {
            try {
                setContent();
                message.setSentDate(new Date());
                var transport = session.getTransport("smtp");
                if (username && password) {
                    transport.connect(host, username, password);
                } else {
                    transport.connect();
                }
                message.saveChanges();
                transport.sendMessage(message, message.getAllRecipients());
            } catch (mx) {
                app.logger.error(errStr + ".send(): " + mx);
                setStatus(SEND);
            } finally {
                if (transport != null && transport.isConnected()) {
                    transport.close();
                }
            }
        } else {
            // no smtp host is given, so write the mail to a file
            this.writeToFile(getProperty("smtp.dir"));
        }
        return;
    };

    /**
     * Main constructor body
     */
    var props = new Properties();
    if (host || (host = getProperty("smtp"))) {
        props.put("mail.transport.protocol", "smtp");
        props.put("mail.smtp.host", String(host));
        props.put("mail.smtp.port", String(port || 25));
        props.put("mail.smtp.starttls.enable",
                getProperty("smtp.tls") || "false");
        props.put("mail.mime.charset",
                getProperty("smtp.charset") ||
                System.getProperty("mail.charset") ||
                "ISO-8859-15");
    }

    this.setAuthentication(getProperty("smtp.username"),
                           getProperty("smtp.password"));
    var session = Session.getInstance(props);
    var message = new MimeMessage(session);

    for (var i in this)
        this.dontEnum(i);

    return this;
}


/** @ignore */
helma.Mail.toString = function() {
    return "[helma.Mail]";
};


/** @ignore */
helma.Mail.prototype.toString = function() {
    return "[helma.Mail Object]";
};

helma.Mail.example = function(host, sender, addr, subject, text) {
    // var smtp = "smtp.host.dom";
    // var sender = "sender@host.dom";
    // var addr = "recipient@host.dom";
    // var subject = "Hello, World!";
    // var text = "This is a test.";
    var port = 25;
    var msg = new helma.Mail(host, port);
    msg.setFrom(sender);
    msg.addTo(addr);
    msg.setSubject(subject);
    msg.setText(text);
    msg.send();
    res.write(msg.status);
    return;
};


helma.lib = "Mail";
helma.dontEnum(helma.lib);
for (var i in helma[helma.lib])
    helma[helma.lib].dontEnum(i);
for (var i in helma[helma.lib].prototype)
    helma[helma.lib].prototype.dontEnum(i);
delete helma.lib;