/*
 * @(#)EmailInspector.java
 *
 * Copyright (c) JSCAPE LLC.
 *
 * This software is the confidential and proprietary information of
 * JSCAPE. ("Confidential Information").  You shall not disclose such
 * Confidential Information and shall use it only in accordance with
 * the terms of the license agreement you entered into with JSCAPE.
 */

package com.jscape.inspect;

import com.jscape.inet.dns.Dns;
import com.jscape.inet.dns.DnsException;
import com.jscape.inet.dns.DnsRequest;
import com.jscape.inet.dns.DnsResponse;
import com.jscape.inet.dns.record.*;
import com.jscape.inet.email.EmailAddress;
import com.jscape.inet.util.TimedSocket;

import java.io.*;
import java.net.InetAddress;
import java.net.Socket;
import java.net.SocketException;
import java.util.*;


/**
 * Performs validation on an email address.
 * <p/>
 * Example Usage:<P>
 * <pre>
 * // create new instance
 * EmailInspector insp = new EmailInspector();
 *
 * // set the DNS server to be used for domain resolution
 * insp.setNameserver("ns1.cox.west.net");
 *
 * // set the level of validation
 * insp.setEmailInspectionLevel(EmailInspector.DOMAIN_VALIDATION);
 *
 * // perform validation of email address
 * try {
 *   insp.validate("user@domain.com");
 * } catch (Exception e) {
 *   e.prinStackTrace(System.out); // validation failed
 * }
 *
 * </pre>
 */

public class EmailInspector
        implements Serializable {
    /**
     * Use in conjunction with <code>setEmailInspectionLevel</code> method to perform syntax validation of e-mail address.
     */
    public static final EmailInspectionLevel SYNTAX_VALIDATION = new EmailInspectionLevel(1);

    /**
     * Use in conjunction with <code>setEmailInspectionLevel</code> method to perform syntax and domain validation of e-mail address.
     */
    public static final EmailInspectionLevel DOMAIN_VALIDATION = new EmailInspectionLevel(2);

    /**
     * Use in conjunction with <code>setEmailInspectionLevel</code> method to perform syntax, domain and mail exchanger validation of e-mail address.
     */
    public static final EmailInspectionLevel MX_VALIDATION = new EmailInspectionLevel(3);

    /**
     * Use in conjunction with <code>setEmailInspectionLevel</code> method to perform syntax, domain, mail exchanger and user validation of e-mail address.
     */
    public static final EmailInspectionLevel USER_VALIDATION = new EmailInspectionLevel(4);


    private static final int SMTP_PORT = 25;
    private EmailInspectionLevel inspectionLevel = SYNTAX_VALIDATION;
    private EmailAddress email = null;
    private String nameServer = "127.0.0.1";
    private Vector denyDomains = new Vector();
    private boolean debug = false;
    private PrintStream debugStream = System.out;
    private File dnsCache = null;
    private Hashtable dnsCacheEntries = null;
    private int dnsCacheDays = 30;
    private boolean ignoreDnsResponseLength = false;
    private int timeout = 10000;
    private String heloAddress = null;
    private String fromAddress = null;


    /**
     * Gets from address used when performing SMTP user validation.
     *
     * @return from address
     */
    public String getFromAddress() {
        return fromAddress;
    }

    /**
     * Sets from address used when performing SMTP user validation.
     *
     * @param fromAddress the from address
     */
    public void setFromAddress(String fromAddress) {
        this.fromAddress = fromAddress;
    }

    /**
     * Constructs a new EmailInspector instance.
     */
    public EmailInspector() {
        
    }

    /**
     * Constructs a new EmailInspector instance.
     *
     * @param inspectionLevel the level of validation to perform
     * @param nameServer      the DNS (Domain Name Server) used in performing domain lookup
     */
    public EmailInspector(EmailInspectionLevel inspectionLevel, String nameServer) {
        this();
        this.inspectionLevel = inspectionLevel;
        this.nameServer = nameServer;
    }

    /**
     * Sets locations of DNS cache file.
     *
     * @param dnsCache location of DNS cache file
     */
    public void setDnsCache(File dnsCache) {
        this.dnsCache = dnsCache;
    }

    /**
     * Gets location of DNS cache file.
     *
     * @return location of DNS cache file
     */
    public File getDnsCache() {
        return dnsCache;
    }

    /**
     * Sets the number of days in which DNS record may live in cache.
     * Default is 30 days.
     *
     * @param dnsCacheDays DNS record cache lifetime in days
     */
    public void setDnsCacheDays(int dnsCacheDays) {
        this.dnsCacheDays = dnsCacheDays;
    }

    /**
     * Sets whether DNS response length header should be ignored when reading response.
     * DNS queries are performed when doing DOMAIN or MX validation.
     * Default is <code>false</code>.
     *
     * @param ignoreDnsResponseLength <code>false</code> if length header is ignored, <code>true</code> otherwise.
     */
    public void setIgnoreDnsResponseLength(boolean ignoreDnsResponseLength) {
        this.ignoreDnsResponseLength = ignoreDnsResponseLength;
    }


    /**
     * Gets whether DNS response length header should be ignored when reading response.
     * DNS queries are performed when doing DOMAIN or MX validation.
     * Default is <code>false</code>.
     *
     * @return <code>false</code> if length header is ignored, <code>true</code> otherwise.
     */
    public boolean getIgnoreDnsResponseLength() {
        return ignoreDnsResponseLength;
    }

    /**
     * Gets the number of days in which DNS record may live in cache.
     * Default is 30 days.
     *
     * @return DNS record cache lifetime in days
     */
    public int getDnsCacheDays() {
        return dnsCacheDays;
    }

    /**
     * Sets debugging state.
     * Default is <code>false</code>.
     * Debugging messages are sent to System.out or alternative debugging stream.
     *
     * @param debug <code>true</code> if debug is enabled
     */
    public void setDebug(boolean debug) {
        this.debug = debug;
    }

    /**
     * Gets debugging state.
     * Default is <code>false</code>.
     * Debugging messages are sent to System.out or alternative debugging stream.
     *
     * @return boolean <code>true</code> if debug is enabled
     */
    public boolean getDebug() {
        return debug;
    }

    /**
     * Sets PrintStream used in reporting debug statements. Default PrintStream
     * is System.out.
     *
     * @param debugStream the PrintStream to send debug statements to
     *
     * @see java.io.PrintStream
     */
    public void setDebugStream(PrintStream debugStream) {
        this.debugStream = debugStream;
    }

    /**
     * Gets PrintStream used in reporting debug statements. Default PrintStream
     * is System.out.
     *
     * @return the PrintStream used in debugging
     *
     * @see java.io.PrintStream
     */
    public PrintStream getDebugStream() {
        return this.debugStream;
    }


    /**
     * Sends debug messages to debug stream.
     *
     * @param str the string to send to debug
     */
    private void debug(String str) {
        if (debug) {
            debugStream.println(str);
        }
    }

    /**
     * Adds domain to list of denied domains.
     * This functionality is very useful in denying e-mail addresses from anonymous e-mail services.
     *
     * @param domain the domain to deny
     */
    public void addDenyDomain(String domain) {
        synchronized (this) {
            denyDomains.addElement(domain.toLowerCase());
        }
    }

    /**
     * Adds contents of <code>InputStream</code> to list of domains to be denied
     * <code>InputStream</code> should be in a format with one (1) domain per line.
     * This functionality is very useful in denying e-mail addresses from anonymous e-mail services.
     *
     * @param fin the input stream to read denied domains from
     *
     * @throws IOException if an error occurs
     */
    private void addDenyDomain(InputStream fin)
            throws IOException {
        BufferedReader bin = new BufferedReader(new InputStreamReader(fin));
        String domain = null;
        while ((domain = bin.readLine()) != null) {
            if (domain.length() > 0) {
                synchronized (this) {
                    denyDomains.addElement(domain.toLowerCase());
                }
            }
        }
        bin.close();
    }

    /**
     * Adds contents of file to list of domains to be denied.
     * File should be in a format with one (1) domain per line.
     * This functionality is very useful in denying e-mail addresses from anonymous e-mail services.
     *
     * @param file the domain file to add to denied domains list
     *
     * @throws IOException if an I/O error occurs
     */
    public void addDenyDomain(File file)
            throws IOException {
        synchronized (this) {
            FileInputStream fin = new FileInputStream(file);
            addDenyDomain(fin);
        }
    }

    /**
     * Clears list of denied domains.
     */
    public void clearDenyDomain() {
        synchronized (this) {
            denyDomains.removeAllElements();
        }
    }

    /**
     * Removes a domain from list of denied domains.
     *
     * @param domain the domain to remove
     */
    public void removeDenyDomain(String domain) {
        synchronized (this) {
            denyDomains.remove(domain.toLowerCase());
        }
    }

    /**
     * Gets an Enumeration of all denied domains.
     *
     * @return Enumeration domains
     */
    public Enumeration getDenyDomain() {
        return denyDomains.elements();
    }


    /**
     * Sets the DNS (Domain Name Server) to be used in performing lookup on a domain.
     * Name server may be represented as an IP address or hostname. Default is 127.0.0.1
     *
     * @param nameServer the DNS nameserver to use for domain queries
     */
    public void setNameserver(String nameServer) {
        synchronized (this) {
            this.nameServer = nameServer;
        }
    }

    /**
     * Gets the DNS (Domain Name Server) to be used in performing lookup on a domain.
     * Name server may be represented as an IP address or hostname. Default is 127.0.0.1
     *
     * @return String nameServer
     */
    public String getNameserver() {
        return nameServer;
    }

    /**
     * Reads response from SMTP server.
     *
     * @param reader the BufferedReader to read response from
     *
     * @return response the response
     *
     * @throws IOException if an I/O error occurs
     */
    private String readResponse(BufferedReader reader)
            throws IOException {
        String response = null;
        // read possible multi-line response
        while (true) {
            response = reader.readLine();
            debug(response);
            if (response == null) {
                break;
            }
            if (!response.substring(3, 4).equals("-")) {
                break;
            }
        }
        if (response == null) {
            return "";
        } else {
            return response;
        }
    }

    /**
     * Sets the validation level to be used in validating an e-mail address.
     * Available validation levels levels are:<code>EmailInspector.SYNTAX_VALIDATION, EmailInspector.DOMAIN_VALIDATION, EmailInspector.MX_VALIDATION, EmailInspector.USER_VALIDATION, EmailInspector.ABSOLUTE_VALIDATION</code>
     * Default validation level is <code>EmailInspector.SYNTAX_VALIDATION</code>.
     *
     * @param inspectionLevel the inspection level to use
     */
    public void setEmailInspectionLevel(EmailInspectionLevel inspectionLevel) {
        synchronized (this) {
            this.inspectionLevel = inspectionLevel;
        }
    }

    /**
     * Gets the validation level to be used in validating an e-mail address.
     *
     * @return EmailInspectionLevel inspectionLevel
     */
    public EmailInspectionLevel getEmailInspectionLevel() {
        return inspectionLevel;
    }

    /**
     * Updates DNS cache.
     *
     * @param domain   the domain to update
     * @param response the DnsResponse
     *
     * @throws IOException            if an error occurs
     * @throws ClassNotFoundException if class is not found
     */
    private void updateCache(String domain, DnsResponse response)
            throws IOException, ClassNotFoundException {
        if (dnsCache != null) {
            ObjectOutputStream out = null;
            try {

                // reinitialize cache in event has changed since validation
                initCache();

                // initialize hashtable if needed
                if (dnsCacheEntries == null) {
                    dnsCacheEntries = new Hashtable();
                }

                // update hashtable
                dnsCacheEntries.put(domain, new DnsResponseCached(response, dnsCacheDays));
                out = new ObjectOutputStream(new FileOutputStream(dnsCache));
                out.writeObject(dnsCacheEntries);
                debug("DNS cache updated.");
            } finally {
                if (out != null) {
                    out.close();
                }
            }
        }
    }

    /**
     * Initializes DNS cache.
     *
     * @throws IOException            if an I/O error occurs
     * @throws ClassNotFoundException if class is not found
     */
    private void initCache()
            throws IOException, ClassNotFoundException {
        if (dnsCacheEntries == null) {
            debug("Reading DNS cache.");
            ObjectInputStream in = null;

            if (dnsCache.exists()) {
                try {
                    in = new ObjectInputStream(new FileInputStream(dnsCache));
                    dnsCacheEntries = (Hashtable) in.readObject();
                } finally {
                    if (in != null) {
                        in.close();
                    }
                }
            }
        }
    }

    /**
     * Attempts to load DnsResponse from cache.
     *
     * @param domain the domain to retrieve response for
     *
     * @return a DnsResponse
     */
    private DnsResponse getDnsResponseCached(String domain) {
        DnsResponse response = null;
        if (dnsCacheEntries != null) {
            debug("Attempting to locate DNS response in cache.");
            DnsResponseCached cachedResponse = (DnsResponseCached) dnsCacheEntries.get(domain);
            if (cachedResponse != null) {
                debug("DNS response found in cache.");
                debug("Checking DNS cache expiration.");
                if (cachedResponse.getDate().before(new Date())) {
                    debug("Cached response expired on " + cachedResponse.getDate() + ". Will perform lookup.");
                } else {
                    response = cachedResponse.getResponse();
                }
            }
        }
        return response;
    }

    /**
     * Checks to see that domain portion of e-mail address is valid.
     *
     * @throws InvalidEmailException thrown if e-mail address is invalid
     */
    private void checkDomain()
            throws InvalidEmailException {
        debug("Initiating DOMAIN level check of e-mail " + email.getEmail());
        DnsResponse response = getDnsResponseCached(email.getDomain());

        // check if response was found in cache
        if (response == null) {
            debug("Connecting to Nameserver " + nameServer);
            Dns dns = new Dns();
            DnsRequest request = new DnsRequest(nameServer, email.getDomain());
            request.setTimeout(timeout);
            request.setRecordType(Dns.TYPE_ANY);

            try {
                response = dns.getResponse(request, ignoreDnsResponseLength);
            } catch (DnsException dne) {
                if (dne.getMessage() == null) {
                    throw new InvalidEmailException("An error occured while performing domain lookup.", email.toString(), dne);
                } else if (dne.getMessage().equals("Name not known (3)")) {
                    throw new InvalidEmailException(email.getEmail(), InvalidEmailException.INVALID_DOMAIN_ERROR);
                } else if (dne.getMessage().equals("Server failure (2)")) {
                    throw new InvalidEmailException(email.getEmail(), InvalidEmailException.INVALID_MX_ERROR);
                } else {
                    throw new InvalidEmailException("An error occured while performing domain lookup.", email.toString(), dne);
                }
            }

            try {
                updateCache(email.getDomain(), response);
            } catch (Exception ioe) {
                debug("Unable to update DNS cache.  " + ioe.getMessage());
            }
        }

        // get all answers from response
        Enumeration answers = response.getAnswers();
        if (answers == null || !answers.hasMoreElements()) {
            throw new InvalidEmailException(email.getEmail(), InvalidEmailException.INVALID_DOMAIN_ERROR);
        } else {
            if (debug) {
                DnsRecord record = (DnsRecord) answers.nextElement();
                debug("Found DNS record for domain " + email.getDomain() + " :" + record.toString());
            }
        }

    }

    /**
     * Checks to see that user portion of e-mail address is valid.
     *
     * @throws InvalidEmailException thrown if e-mail address is invalid
     */
    private void checkUser()
            throws InvalidEmailException {
        Socket con = null;
        String responseCode = null;
        String smtpHostname = checkMailExchanger();
        try {
            debug("Initiating USER level check of e-mail " + email.getEmail());
            /* Create Socket connections and data streams */
            debug("Connecting to SMTP server " + smtpHostname);
            con = TimedSocket.getSocket(smtpHostname, SMTP_PORT, timeout);
            BufferedReader smtpIn = new BufferedReader(new InputStreamReader(con.getInputStream()));
            DataOutputStream smtpOut = new DataOutputStream(con.getOutputStream());
            responseCode = readResponse(smtpIn);

            /* Login to SMTP server */
            String ip = this.heloAddress != null ? this.heloAddress : InetAddress.getLocalHost().getHostName();
            smtpOut.writeBytes("HELO " + ip + "\r\n");
            smtpOut.flush();
            debug("HELO " + ip);
            responseCode = readResponse(smtpIn);

            /* Perform Loose User Verification */

            /* Send MAIL FROM */
            if (fromAddress != null) {
                smtpOut.writeBytes("MAIL FROM:<" + fromAddress + ">\r\n");
                debug("MAIL FROM:<" + fromAddress + ">");
            } else {
                smtpOut.writeBytes("MAIL FROM:<" + email.getEmail() + ">\r\n");
                debug("MAIL FROM:<" + email.getEmail() + ">");
            }
            smtpOut.flush();

            /* Get Response Code */
            responseCode = readResponse(smtpIn);
            if (responseCode.startsWith("5")) {
                throw new InvalidEmailException(email.getEmail(), InvalidEmailException.INVALID_USER_ERROR);
            }

            /* Send RCPT TO */
            smtpOut.writeBytes("RCPT TO:<" + email.getEmail() + ">\r\n");
            smtpOut.flush();
            debug("RCPT TO:<" + email.getEmail() + ">");

            /* Get Response Code */
            responseCode = readResponse(smtpIn);

            if (responseCode.startsWith("5")) {
                throw new InvalidEmailException(email.getEmail(), InvalidEmailException.INVALID_USER_ERROR);
            }
        } catch (SocketException rhe) {
            throw new InvalidEmailException("Unable to connect to SMTP server " + smtpHostname + " or connection dropped",
                                            email.toString(), rhe);
        } catch (IOException ioe) {
            throw new InvalidEmailException("An error occured while performing validating user", email.toString(), ioe);
        } finally {
            if (con != null) {
                try {
                    con.close();
                } catch (IOException ioe) {
                    // ignore
                }
            }
        }
    }

    /**
     * Checks to see that syntax of e-mail address is valid.
     *
     * @throws InvalidEmailException thrown if e-mail address is invalid
     */
    private void checkSyntax()
            throws InvalidEmailException {
        debug("Initiating SYNTAX level check of e-mail " + email.getEmail());
        if (email != null) {
            if (!email.isValid()) {
                throw new InvalidEmailException(email.getEmail(), InvalidEmailException.INVALID_SYNTAX_ERROR);
            }
        } else {
            throw new InvalidEmailException("", InvalidEmailException.INVALID_SYNTAX_ERROR);
        }
    }

    /**
     * Checks to see that domain of e-mail address is valid.
     *
     * @throws InvalidEmailException thrown if e-mail address domain is invalid
     */
    private void checkDeniedDomain()
            throws InvalidEmailException {
        if (denyDomains.contains(email.getDomain().toLowerCase())) {
            throw new InvalidEmailException(email.getEmail(), InvalidEmailException.DENIED_DOMAIN_ERROR);
        }
    }

    /**
     * Gets SMTP server hostname for email address.
     *
     * @return the SMTP server hostname
     *
     * @throws InvalidEmailException if an error occurs
     */
    private String checkMailExchanger()
            throws InvalidEmailException {
        debug("Initiating MX level check of e-mail " + email.getEmail());
        DnsResponse response = getDnsResponseCached(email.getDomain());

        if (response == null) {
            debug("Connecting to Nameserver " + nameServer);
            Dns dns = new Dns();
            DnsRequest request = new DnsRequest(nameServer, email.getDomain());
            request.setTimeout(timeout);
            request.setRecordType(Dns.TYPE_MX);

            try {
                response = dns.getResponse(request, ignoreDnsResponseLength);
            } catch (DnsException dne) {
                if (dne.getMessage() == null) {
                    throw new InvalidEmailException("An error occured while performing domain lookup.", email.toString(), dne);
                } else if (dne.getMessage().equals("Name not known (3)")) {
                    throw new InvalidEmailException(email.getEmail(), InvalidEmailException.INVALID_DOMAIN_ERROR);
                } else if (dne.getMessage().equals("Server failure (2)")) {
                    throw new InvalidEmailException(email.getEmail(), InvalidEmailException.INVALID_MX_ERROR);
                } else {
                    throw new InvalidEmailException("An error occured while performing domain lookup.", email.toString(), dne);
                }
            }

            // update cache
            try {
                updateCache(email.getDomain(), response);
            } catch (Exception ioe) {
                debug("Unable to update DNS cache.  " + ioe.getMessage());
            }
        }
        // get SMTP hostname
        return getSmtpHostname(response);
    }

    /**
     * Gets SMTP hostname for a DnsResponse.
     *
     * @param response a DnsResponse
     *
     * @return an SMTP server hostname
     *
     * @throws InvalidEmailException if an error occurs
     */
    private String getSmtpHostname(DnsResponse response)
            throws InvalidEmailException {
        String smtpHostname = null;

        // get DNS response answers
        Enumeration answers = response.getAnswers();

        // no MX records found .. try to connect to domain port 25
        if (answers == null || !answers.hasMoreElements()) {
            try {
                debug("No MX record found.  Attempting to locate SMTP server");
                Address address = null;
                Dns dns = new Dns();
                DnsRequest request = new DnsRequest(nameServer, email.getDomain());
                request.setTimeout(timeout);
                request.setRecordType(Dns.TYPE_A);
                response = dns.getResponse(request, ignoreDnsResponseLength);
                answers = response.getAnswers();

                // loop thru all DNS records
                while (answers.hasMoreElements()) {
                    DnsRecord obj = (DnsRecord) answers.nextElement();
                    debug("Found DNS record: " + obj.toString());
                    if (obj instanceof Address) {
                        address = (Address) obj;
                        break;
                    }
                }

                if (address != null) {
                    InetAddress inetAddr = address.getInetAddress();
                    debug("Attempting to connect to port 25 for Address record with IP: " + inetAddr);
                    Socket s = new Socket(inetAddr, 25);
                    InputStream in = s.getInputStream();
                    if (in != null) {
                        smtpHostname = inetAddr.getHostAddress();
                        debug("Established connection on port 25 with address " + smtpHostname);
                        in.close();
                    } else {
                        throw new InvalidEmailException(email.getEmail(), InvalidEmailException.INVALID_MX_ERROR);
                    }
                } else {
                    debug("Unable to resolve Address record for domain: " + email.getDomain());
                    throw new InvalidEmailException(email.getEmail(), InvalidEmailException.INVALID_MX_ERROR);
                }
            } catch (Exception e) {
                throw new InvalidEmailException(email.getEmail(), InvalidEmailException.INVALID_MX_ERROR);
            }
        } else {
            Object[] exchanges = null;
            Vector tmpAnswers = new Vector();
            while (answers.hasMoreElements()) {
                tmpAnswers.add(answers.nextElement());
            }
            exchanges = tmpAnswers.toArray();

            /* Sort MX records by preference */
            Arrays.sort(exchanges, new MailExchangerComparator());

            DnsRecord record = (DnsRecord) exchanges[0];

            if (!(record instanceof MailExchanger)) {
                for (int i = 1; i < exchanges.length; ++i) {
                    DnsRecord tmpRecord = (DnsRecord) exchanges[i];
                    if (tmpRecord instanceof MailExchanger) {
                        record = tmpRecord;
                        break;
                    }
                }
            }

            if (record instanceof MailExchanger) {
                smtpHostname = ((MailExchanger) record).getMX();
            } else if (record instanceof CanonicalName) {
                smtpHostname = ((CanonicalName) record).getCanonicalName();
            } else if (record instanceof Address) {
                try {
                    InetAddress addr = (InetAddress) ((Address) record).getInetAddress();
                    smtpHostname = addr.getHostName();
                } catch (Exception e) {
                    // todo: ignore?
                }
            }
            debug("Located SMTP hostname " + smtpHostname);
        }
        return smtpHostname;
    }

    /**
     * Performs validation of e-mail address based on level of validation defined in call to <code>setEmailInspectionLevel</code>.
     *
     * @param email the email to validate
     *
     * @throws InvalidEmailException thrown if e-mail address is invalid
     */
    public void validate(String email)
            throws InvalidEmailException {
        synchronized (this) {
            try {
                initCache();
            } catch (Exception ioe) {
                debug("Unable to load DNS cache. " + ioe.getMessage());
            }

            this.email = new EmailAddress(email);
            debug("Validation started");
            long start = System.currentTimeMillis();
            switch (inspectionLevel.getLevel()) {
                case 1:
                    checkSyntax();
                    break;
                case 2:
                    checkSyntax();
                    checkDeniedDomain();
                    checkDomain();
                    break;
                case 3:
                    checkSyntax();
                    checkDeniedDomain();
                    checkMailExchanger();
                    break;
                case 4:
                    checkSyntax();
                    checkDeniedDomain();
                    checkUser();
                    break;
                default:
                    break;
            }
            long end = System.currentTimeMillis();
            debug("Validation completed in " + (end - start) + " milliseconds");
        }
    }

    /**
     * Gets timeout in milliseconds for connecting to DNS and SMTP servers.
     *
     * @return timeout in milliseconds
     */
    public int getTimeout() {
        return timeout;
    }

    /**
     * Sets timeout in milliseconds for connecting to DNS and SMTP servers.
     *
     * @param timeout the timeout in milliseconds
     */
    public void setTimeout(int timeout) {
        this.timeout = timeout;
    }

    /**
     * Gets HELO hostname or IP address used when communicating with SMTP server.
     * By default local hostname is used.
     *
     * @return the hostname or IP address to use
     */
    public String getHeloAddress() {
        return heloAddress;
    }

    /**
     * Sets HELO hostname or IP address used when communicating with SMTP server.
     * By default local hostname is used.
     *
     * @param heloAddress the hostname or IP address to use
     */
    public void setHeloAddress(String heloAddress) {
        this.heloAddress = heloAddress;
    }
}

