SmtpEmail.java
/*
** Module : SmtpEmail.java
** Abstract : An implementation of the SMTP EMAIL FWD extension to the 4GL.
**
** Copyright (c) 2017-2024, Golden Code Development Corporation.
**
** -#- -I- --Date-- --------------------------------Description-----------------------------------
** 001 GES 20171026 First commit.
** 002 CA 20240918 Removed default methods from interfaces and moved them as concrete implementations, as
** default methods cause the Java metaspace to spike one order of magnitude.
** 003 GBB 20241009 Access the mail sender via OSResourceManager.
*/
/*
** This program is free software: you can redistribute it and/or modify
** it under the terms of the GNU Affero General Public License as
** published by the Free Software Foundation, either version 3 of the
** License, or (at your option) any later version.
**
** This program is distributed in the hope that it will be useful,
** but WITHOUT ANY WARRANTY; without even the implied warranty of
** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
** GNU Affero General Public License for more details.
**
** You may find a copy of the GNU Affero GPL version 3 at the following
** location: https://www.gnu.org/licenses/agpl-3.0.en.html
**
** Additional terms under GNU Affero GPL version 3 section 7:
**
** Under Section 7 of the GNU Affero GPL version 3, the following additional
** terms apply to the works covered under the License. These additional terms
** are non-permissive additional terms allowed under Section 7 of the GNU
** Affero GPL version 3 and may not be removed by you.
**
** 0. Attribution Requirement.
**
** You must preserve all legal notices or author attributions in the covered
** work or Appropriate Legal Notices displayed by works containing the covered
** work. You may not remove from the covered work any author or developer
** credit already included within the covered work.
**
** 1. No License To Use Trademarks.
**
** This license does not grant any license or rights to use the trademarks
** Golden Code, FWD, any Golden Code or FWD logo, or any other trademarks
** of Golden Code Development Corporation. You are not authorized to use the
** name Golden Code, FWD, or the names of any author or contributor, for
** publicity purposes without written authorization.
**
** 2. No Misrepresentation of Affiliation.
**
** You may not represent yourself as Golden Code Development Corporation or FWD.
**
** You may not represent yourself for publicity purposes as associated with
** Golden Code Development Corporation, FWD, or any author or contributor to
** the covered work, without written authorization.
**
** 3. No Misrepresentation of Source or Origin.
**
** You may not represent the covered work as solely your work. All modified
** versions of the covered work must be marked in a reasonable way to make it
** clear that the modified work is not originating from Golden Code Development
** Corporation or FWD. All modified versions must contain the notices of
** attribution required in this license.
*/
package com.goldencode.p2j.email;
import com.goldencode.p2j.net.*;
import com.goldencode.p2j.security.*;
import com.goldencode.p2j.util.*;
import com.goldencode.p2j.*;
import com.goldencode.email.*;
import com.goldencode.p2j.util.osresource.*;
import org.apache.commons.mail.*;
import java.net.*;
/**
* Implementation of the SMTP EMAIL extension. An {@code SmtpEmail} object is a handle based
* resource with attributes and methods that can be used to define the email's state and to send
* the email. The sending will occur on the client.
* <p>
* This is a FWD-specific set of functionality. It does not exist and will not run in the
* Progress 4GL.
* <p>
* To create an SMTP-EMAIL resource use the following syntax:
* <pre>
* CREATE SMTP-EMAIL handle [ASSIGN { ATTR_OR_METHOD = value ...}].
* </pre>
* The following list of attributes are available:
* <pre>
* Attribute Data Type Required Description
* ---------------- ------------ -------- ------------------------------------------------------------------------------------------------
* connection-type character Y Must be "UNENCRYPTED", "STARTTLS_WHEN_AVAILABLE", "STARTTLS_REQUIRED" or "SSL".
* smtp-from character Y This is the FROM address (the email address of the sender).
* smtp-host character Y This is the SMTP server's IP address or hostname.
* smtp-html character N If set, this is the content of the HTML body.
* smtp-port character N Defaults to 25 for UNENCRYPTED, 587 for STARTTLS_* and 465 for SSL.
* smtp-reply-to character N Optional REPLY-TO address.
* smtp-subject character Y This is the subject line.
* smtp-text character N If set, this is the content of the HTML body.
* smtp-user character N If both the user and the password are NOT set, then no authentication will be attempted. If set, this is the userid for authentication with the SMTP server.
* smtp-password character N If both the user and the password are NOT set, then no authentication will be attempted. If set, this is the password for authentication with the SMTP server.
* smtp-validate logical N Defaults to TRUE. If FALSE, the SMTP server's SSL certificate will be treated as trusted. Ignored for UNENCRYPTED mode.
* </pre>
* <p>
* The following list of methods are available:
* <pre>
* Method Return Type Return Meaning Required Description
* -------------------------------- ----------- -------------- -------- -----------------------------------------------------------------------------------------------------
* add-to-address(character addr) logical TRUE on success. N This adds a TO address to the mail. At least one recipient is needed.
* add-cc-address(character addr) logical TRUE on success. N This adds a CC address to the mail. At least one recipient is needed.
* add-bcc-address(character addr) logical TRUE on success. N This adds a BCC address to the mail. At least one recipient is needed.
* attach-file(character filename) logical TRUE on success. N This attaches a local file to the email. The file must exist and be accessible to the user.
* attach-file(character url) logical TRUE on success. N This attaches a resource specified by URL to the email. The resource will be downloaded when the email is sent. The URL must exist and be accessible to the user.
* clear-attachments-list() logical TRUE on success. N Deletes everything in the attachments list.
* clear-bcc-list() logical TRUE on success. N Deletes everything in the BCC address list.
* clear-cc-list() logical TRUE on success. N Deletes everything in the CC address list.
* clear-embedded-image-list() logical TRUE on success. N Deletes everything in the embedded image list.
* clear-to-list() logical TRUE on success. N Deletes everything in the TO address list.
* embed-file(character filename) character CONTENT-ID N Adds a local file to the embedded image list and returns the content ID to be substituted into the IMG tag of the HTML body. The file must exist and be accessible to the user.
* embed-url(character filename) character CONTENT-ID N Adds a URL resource to the embedded image list and returns the content ID to be substituted into the IMG tag of the HTML body. The resource will be downloaded when the email is sent. The URL must exist and be accessible to the user.
* get-attachments-list() character[] List as an array. N Returns the attachments list as an array.
* get-bcc-list() character[] List as an array. N Returns the BCC address list as an array.
* get-cc-list() character[] List as an array. N Returns the CC address list as an array.
* get-embedded-image-list() character[] List as an array. N Returns the embedded image list as an array.
* get-to-list() character[] List as an array. N Returns the TO address list as an array.
* send() logical TRUE on success. Y Sends the email as previously defined. You can call this more than once (possibly changing the state of the handle each time). Every time this is called it will attempt to send the defined email. This may raise an ERROR condition if there are any problems during send.
* </pre>
* <p>
* At the end of lifetime of the object, it should be deleted, as usual with
* <pre>DELETE OBJECT handle.</pre>
*/
public class SmtpEmail
implements WrappedResource,
EmailSender,
Deletable
{
/** Stores context-local state variables. */
private static ContextContainer remoteSender = new ContextContainer();
/** The resource id. Used by {@code WrappedResource} management. */
private Long resourceId = null;
/** Flag to determine if this instance has been deleted. */
private boolean valid = true;
/** Serializable container for email state. */
private EmailDefinition def = new EmailDefinition();
/**
* Construct an instance.
*/
public SmtpEmail()
{
handle.resourceId(this);
}
/**
* Static factory method for initialization of a fresh new SMTP EMAIL as an extension object.
*
* @param h
* The {@link handle} to store the newly created object.
*/
public static void create(handle h)
{
h.assign(new SmtpEmail());
}
/**
* Reports if this object is unknown.
*
* @return <code>true</code> if object is unknown.
*/
@Override
public boolean unknown()
{
return false;
}
/**
* Get this resource's ID, if is already set.
*
* @return The resource's ID or <code>null</code> if not set.
*/
@Override
public Long id()
{
return resourceId;
}
/**
* Set this resource's ID.
*
* @param id
* The resource's ID.
*/
@Override
public void id(long id)
{
resourceId = id;
}
/**
* Perform actual delete of all resources. At the time of this call, it is assumed the resource
* is valid for deletion (the handle and the resource are both valid).
* <p>
* This is called from a handle when working as FWD extension object.
*/
@Override
public void delete()
{
if (!valid)
{
return; // already deleted
}
// end of lifetime: mark the object are invalid
valid = false;
}
/**
* Check if the resource can veto the delete or not, before attempting the {@link #delete()} call.
*
* @param ref
* The exact handle holding the reference, on which the delete is attempted.
* @param explicit
* Flag indicating this is called via <code>DELETE OBJECT</code> from the application.
*
* @return <code>false</code> if the delete is vetoed.
*/
@Override
public boolean allowDelete(handle ref, boolean explicit)
{
return true;
}
/**
* Checks whether this object is valid. The object is valid from the moment it was created
* (even if not active) and until expressly deleted.
*
* @return {@code true} if this object is valid.
*/
@Override
public boolean valid()
{
return valid;
}
/**
* Gets the connection type for the SMTP communications session.
*
* @return The connection type which is "UNENCRYPTED", "STARTTLS_WHEN_AVAILABLE",
* "STARTTLS_REQUIRED" or "SSL".
*/
public character getConnectionType()
{
return new character(def.connectType.toString());
}
/**
* Sets the connection type for the SMTP communications session.
*
* @param type
* The connection type which must be "UNENCRYPTED", "STARTTLS_WHEN_AVAILABLE",
* "STARTTLS_REQUIRED" or "SSL". Must not be <code>null</code> or unknown.
*/
public void setConnectionType(character type)
{
if (type == null || type.isUnknown())
{
return;
}
setConnectionType(type.toStringMessage());
}
/**
* Sets the connection type for the SMTP communications session.
*
* @param type
* The connection type which must be "UNENCRYPTED", "STARTTLS_WHEN_AVAILABLE",
* "STARTTLS_REQUIRED" or "SSL". Must not be <code>null</code>.
*/
public void setConnectionType(String type)
{
if (type == null || type.length() == 0)
{
return;
}
try
{
def.connectType = ConnectionSecurity.valueOf(type);
}
catch (IllegalArgumentException iae)
{
String msg = String.format("Invalid connection type '%s'.", type);
ErrorManager.recordOrThrowError(new NumberedException(msg, -1, iae));
}
}
/**
* Gets the host for the SMTP server.
*
* @return The name or IP address of the SMTP server.
*/
public character getSmtpHost()
{
return (def.host == null) ? new character() : new character(def.host);
}
/**
* Sets the host for the SMTP server.
*
* @param host
* The name or IP address of the SMTP server. Must not be <code>null</code>
* or unknown.
*/
public void setSmtpHost(character host)
{
if (host == null || host.isUnknown())
{
return;
}
setSmtpHost(host.toStringMessage());
}
/**
* Sets the host for the SMTP server.
*
* @param host
* The name or IP address of the SMTP server. Must not be <code>null</code>.
*/
public void setSmtpHost(String host)
{
if (host == null || host.length() == 0)
{
return;
}
def.host = host;
}
/**
* Gets the port for the SMTP server.
*
* @return The port used by the SMTP server.
*/
public character getSmtpPort()
{
return (def.port == null) ? new character() : new character(def.port);
}
/**
* Sets the port for the SMTP server.
*
* @param port
* The port used by the SMTP server.
*/
public void setSmtpPort(character port)
{
if (port == null || port.isUnknown())
{
return;
}
setSmtpPort(port.toStringMessage());
}
/**
* Sets the port for the SMTP server.
*
* @param port
* The port used by the SMTP server.
*/
public void setSmtpPort(String port)
{
if (port == null || port.length() == 0)
{
return;
}
def.port = port;
}
/**
* Gets the userid for authenticating with the SMTP server.
*
* @return The userid.
*/
public character getSmtpUser()
{
return (def.userid == null) ? new character() : new character(def.userid);
}
/**
* Sets the userid for authenticating with the SMTP server.
*
* @param userid
* The userid for authentication.
*/
public void setSmtpUser(character userid)
{
if (userid == null || userid.isUnknown())
{
return;
}
setSmtpUser(userid.toStringMessage());
}
/**
* Sets the userid for authenticating with the SMTP server.
*
* @param userid
* The userid for authentication.
*/
public void setSmtpUser(String userid)
{
if (userid == null || userid.length() == 0)
{
return;
}
def.userid = userid;
}
/**
* Gets the password for authenticating with the SMTP server.
*
* @return The password.
*/
public character getSmtpPassword()
{
return (def.password == null) ? new character() : new character(def.password);
}
/**
* Sets the password for authenticating with the SMTP server.
*
* @param password
* The password for authentication.
*/
public void setSmtpPassword(character password)
{
if (password == null || password.isUnknown())
{
return;
}
setSmtpPassword(password.toStringMessage());
}
/**
* Sets the password for authenticating with the SMTP server.
*
* @param password
* The password for authentication.
*/
public void setSmtpPassword(String password)
{
if (password == null || password.length() == 0)
{
return;
}
def.password = password;
}
/**
* Returns the state of the SMTP-VALIDATE attribute, which determines if the SMTP server
* certificate is validated during an SSL or STARTTLS connection.
*
* @return Current state of the SMTP-VALIDATE attribute.
*/
public logical isSmtpValidate()
{
return new logical(def.validate);
}
/**
* Setter for SMTP-VALIDATE attribute, which determines if the SMTP server certificate is
* validated during an SSL or STARTTLS connection.
*
* @param validate
* <code>true</code> to validate the server certificate.
*/
public void setSmtpValidate(boolean validate)
{
def.validate = validate;
}
/**
* Setter for SMTP-VALIDATE attribute, which determines if the SMTP server certificate is
* validated during an SSL or STARTTLS connection.
*
* @param validate
* <code>true</code> to validate the server certificate.
*/
public void setSmtpValidate(logical validate)
{
if (validate == null || validate.isUnknown())
{
return;
}
setSmtpValidate(validate.booleanValue());
}
/**
* Gets the FROM address.
*
* @return The FROM address.
*/
public character getSmtpFrom()
{
return (def.from == null) ? new character() : new character(def.from);
}
/**
* Sets the FROM address.
*
* @param from
* The FROM address.
*/
public void setSmtpFrom(character from)
{
if (from == null || from.isUnknown())
{
return;
}
setSmtpFrom(from.toStringMessage());
}
/**
* Sets the FROM address.
*
* @param from
* The FROM address.
*/
public void setSmtpFrom(String from)
{
if (from == null || from.length() == 0)
{
return;
}
def.from = from;
}
/**
* Gets the REPLY-TO address.
*
* @return The REPLY-TO address.
*/
public character getSmtpReplyTo()
{
return (def.replyTo == null) ? new character() : new character(def.replyTo);
}
/**
* Sets the REPLY-TO address.
*
* @param replyTo
* The REPLY-TO address.
*/
public void setSmtpReplyTo(character replyTo)
{
if (replyTo == null || replyTo.isUnknown())
{
return;
}
setSmtpReplyTo(replyTo.toStringMessage());
}
/**
* Sets the REPLY-TO address.
*
* @param replyTo
* The REPLY-TO address.
*/
public void setSmtpReplyTo(String replyTo)
{
if (replyTo == null || replyTo.length() == 0)
{
return;
}
def.replyTo = replyTo;
}
/**
* Gets the SUBJECT line.
*
* @return The SUBJECT line.
*/
public character getSmtpSubject()
{
return (def.subject == null) ? new character() : new character(def.subject);
}
/**
* Sets the SUBJECT line.
*
* @param subject
* The SUBJECT line.
*/
public void setSmtpSubject(character subject)
{
if (subject == null || subject.isUnknown())
{
return;
}
setSmtpSubject(subject.toStringMessage());
}
/**
* Sets the SUBJECT line.
*
* @param subject
* The SUBJECT line.
*/
public void setSmtpSubject(String subject)
{
if (subject == null || subject.length() == 0)
{
return;
}
def.subject = subject;
}
/**
* Gets the HTML body contents.
*
* @return The HTML body contents.
*/
public character getSmtpHtmlBody()
{
return (def.html == null) ? new character() : new character(def.html);
}
/**
* Sets the HTML body contents.
*
* @param html
* The HTML body contents.
*/
public void setSmtpHtmlBody(character html)
{
if (html == null || html.isUnknown())
{
return;
}
setSmtpHtmlBody(html.toStringMessage());
}
/**
* Sets the HTML body contents.
*
* @param html
* The HTML body contents.
*/
public void setSmtpHtmlBody(String html)
{
if (html == null || html.length() == 0)
{
return;
}
def.html = html;
}
/**
* Gets the TEXT body contents.
*
* @return The TEXT body contents.
*/
public character getSmtpTextBody()
{
return (def.text == null) ? new character() : new character(def.text);
}
/**
* Sets the TEXT body contents.
*
* @param text
* The TEXT body contents.
*/
public void setSmtpTextBody(character text)
{
if (text == null || text.isUnknown())
{
return;
}
setSmtpTextBody(text.toStringMessage());
}
/**
* Sets the TEXT body contents.
*
* @param text
* The TEXT body contents.
*/
public void setSmtpTextBody(String text)
{
if (text == null || text.length() == 0)
{
return;
}
def.text = text;
}
/**
* Add a TO address to the list of recipients.
*
* @param addr
* The TO address.
*/
public logical addToAddress(character addr)
{
if (addr == null || addr.isUnknown())
{
return new logical(false);
}
return addToAddress(addr.toStringMessage());
}
/**
* Add a TO address to the list of recipients.
*
* @param addr
* The TO address.
*/
public logical addToAddress(String addr)
{
if (addr == null || addr.length() == 0)
{
return new logical(false);
}
def.addTo(addr);
return new logical(true);
}
/**
* Add a CC address to the list of recipients.
*
* @param addr
* The CC address.
*/
public logical addCcAddress(character addr)
{
if (addr == null || addr.isUnknown())
{
return new logical(false);
}
return addCcAddress(addr.toStringMessage());
}
/**
* Add a CC address to the list of recipients.
*
* @param addr
* The CC address.
*/
public logical addCcAddress(String addr)
{
if (addr == null || addr.length() == 0)
{
return new logical(false);
}
def.addCc(addr);
return new logical(true);
}
/**
* Add a BCC address to the list of recipients.
*
* @param addr
* The BCC address.
*/
public logical addBccAddress(character addr)
{
if (addr == null || addr.isUnknown())
{
return new logical(false);
}
return addBccAddress(addr.toStringMessage());
}
/**
* Add a BCC address to the list of recipients.
*
* @param addr
* The BCC address.
*/
public logical addBccAddress(String addr)
{
if (addr == null || addr.length() == 0)
{
return new logical(false);
}
def.addBcc(addr);
return new logical(true);
}
/**
* Attach the specified file to the email.
*
* @param file
* The file to attach.
*/
public logical attachFile(character file)
{
if (file == null || file.isUnknown())
{
return new logical(false);
}
return attachFile(file.toStringMessage());
}
/**
* Attach the specified file to the email.
*
* @param file
* The file to attach.
*/
public logical attachFile(String file)
{
if (file == null || file.length() == 0)
{
return new logical(false);
}
def.attachFile(file);
return new logical(true);
}
/**
* Attach the file specified by the given URL to the email.
*
* @param url
* The resource to attach.
*/
public logical attachURL(character url)
{
if (url == null || url.isUnknown())
{
return new logical(false);
}
return attachURL(url.toStringMessage());
}
/**
* Attach the file specified by the given URL to the email.
*
* @param url
* The resource to attach.
*/
public logical attachURL(String url)
{
if (url == null || url.length() == 0)
{
return new logical(false);
}
def.attachURL(url);
return new logical(true);
}
/**
* Clear the list of attachments.
*/
public logical clearAttachmentsList()
{
def.clearAttachmentsList();
return new logical(true);
}
/**
* Clear the list of BCC recipients.
*/
public logical clearBccAddressList()
{
def.clearBccList();
return new logical(true);
}
/**
* Clear the list of CC recipients.
*/
public logical clearCcAddressList()
{
def.clearCcList();
return new logical(true);
}
/**
* Clear the list of embedded images.
*/
public logical clearEmbeddedImageList()
{
def.clearEmbedsList();
return new logical(true);
}
/**
* Clear the list of TO recipients.
*/
public logical clearToAddressList()
{
def.clearToList();
return new logical(true);
}
/**
* Embed the image specified by the given file and return the content ID needed to substitute
* into the HTML (in an IMG tag). The result will be visible inline in the HTML document as
* part of the IMG.
*
* @param file
* The resource to attach.
*
* @return The content ID for use in an IMG tag.
*/
public character embedImageByFile(character file)
{
if (file == null || file.isUnknown())
{
return new character();
}
return embedImageByFile(file.toStringMessage());
}
/**
* Embed the image specified by the given file and return the content ID needed to substitute
* into the HTML (in an IMG tag). The result will be visible inline in the HTML document as
* part of the IMG.
*
* @param file
* The resource to attach.
*
* @return The content ID for use in an IMG tag.
*/
public character embedImageByFile(String file)
{
if (file == null || file.length() == 0)
{
return new character();
}
return new character(def.embedFile(file));
}
/**
* Embed the image specified by the given URL and return the content ID needed to substitute
* into the HTML (in an IMG tag). The result will be visible inline in the HTML document as
* part of the IMG.
*
* @param url
* The resource to attach.
*
* @return The content ID for use in an IMG tag.
*/
public character embedImageByURL(character url)
{
if (url == null || url.isUnknown())
{
return new character();
}
return embedImageByURL(url.toStringMessage());
}
/**
* Embed the image specified by the given URL and return the content ID needed to substitute
* into the HTML (in an IMG tag). The result will be visible inline in the HTML document as
* part of the IMG.
*
* @param url
* The resource to attach.
*
* @return The content ID for use in an IMG tag.
*/
public character embedImageByURL(String url)
{
if (url == null || url.length() == 0)
{
return new character();
}
return new character(def.embedURL(url, "image"));
}
/**
* Obtain the list of attachments.
*
* @return The array of attachment file and URL names.
*/
public character[] getAttachmentsList()
{
AttachmentDescriptor[] list = def.getAttachmentsList();
String[] slist = new String[list.length];
for (int i = 0; i < list.length; i++)
{
slist[i] = list[i].resource;
}
return stringArrayToCharacterArray(slist);
}
/**
* Obtain the list of BCC recipients.
*
* @return The array of all BCC recipients.
*/
public character[] getBccAddressList()
{
return stringArrayToCharacterArray(def.getBccList());
}
/**
* Obtain the list of CC recipients.
*
* @return The array of all CC recipients.
*/
public character[] getCcAddressList()
{
return stringArrayToCharacterArray(def.getCcList());
}
/**
* Obtain the list of embedded image file and URL names.
*
* @return The array of attachment file and URL names.
*/
public character[] getEmbeddedImageList()
{
EmbedDescriptor[] list = def.getEmbedsList();
String[] slist = new String[list.length];
for (int i = 0; i < list.length; i++)
{
slist[i] = list[i].resource;
}
return stringArrayToCharacterArray(slist);
}
/**
* Obtain the list of TO recipients.
*
* @return The array of all TO recipients.
*/
public character[] getToAddressList()
{
return stringArrayToCharacterArray(def.getToList());
}
/**
* Send the email.
*
* @return <code>true</code> if the send succeeded.
*
* @throws ErrorConditionException
* On any failure, unless silent error mode is enabled.
*/
public logical send()
throws ErrorConditionException
{
try
{
// use the remote object to invoke on the client
remoteSender.obtain().send(def);
}
catch (RuntimeException | EmailException | MalformedURLException ex)
{
ErrorManager.recordOrThrowError(new NumberedException("Failure during email send().", -1, ex));
return new logical(false);
}
return new logical(true);
}
/**
* Convert the given string array into a charaacter array.
*
* @param list
* Input array.
*
* @return The output array.
*/
private character[] stringArrayToCharacterArray(String[] list)
{
character[] slist = new character[list.length];
for (int i = 0; i < list.length; i++)
{
slist[i] = new character(list[i]);
}
return slist;
}
/**
* Simple container that stores and returns a context-local instance of
* the remote email sender.
*/
private static class ContextContainer
extends ContextLocal<RemoteEmailSender>
{
/**
* Obtains the context-local instance of the contained global work
* area.
*
* @return The remote email sender associated with this context.
*/
public RemoteEmailSender obtain()
{
return this.get();
}
/**
* Initializes the remote email sender, the first time it is requested within a
* new context.
*
* @return The newly instantiated remote email sender.
*/
protected synchronized RemoteEmailSender initialValue()
{
return OSResourceManager.getEmailSender();
}
}
}