Skip to content

Commit

Permalink
Add support for AWS SES as mail sending provider
Browse files Browse the repository at this point in the history
This commits adds the support for AWS Simple Email Service as an alternative
transport for sending notification emails from nextflow.

The email is sent via AWS SES when using the nf-amazon plugin, and:
- the NXF_ENABLE_AWS_SES=true environment variable is set
- or, not `mail.smtp` setting is provided and the AWS_REGION or AWS_DEFAULT_REGION is set
  in the launching environment

Signed-off-by: Paolo Di Tommaso <[email protected]>
  • Loading branch information
pditommaso committed Jun 2, 2023
1 parent 1daebee commit df85d44
Show file tree
Hide file tree
Showing 12 changed files with 369 additions and 88 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
/*
* Copyright 2020-2023, Seqera Labs
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
*/

package nextflow.mail

import javax.mail.MessagingException
import javax.mail.internet.MimeMessage

import groovy.transform.CompileStatic
import nextflow.util.Duration

/**
* Base class or sending an email via sys command `mail` or `sendmail`
*
* @author Paolo Di Tommaso <[email protected]>
*/
@CompileStatic
abstract class BaseMailProvider implements MailProvider {

private long SEND_MAIL_TIMEOUT = 15_000

/**
* Send a email message by using system tool such as `sendmail` or `mail`
*
* @param message A {@link MimeMessage} object representing the email to send
*/
void send(MimeMessage message, Mailer mailer) {
final cmd = [name(), '-t']
final proc = new ProcessBuilder()
.command(cmd)
.redirectErrorStream(true)
.start()
// pipe the message to the sendmail stdin
final stdout = new StringBuilder()
final stdin = proc.getOutputStream()
message.writeTo(stdin);
stdin.close() // <-- don't forget otherwise it hangs
// wait for the sending to complete
final consumer = proc.consumeProcessOutputStream(stdout)
proc.waitForOrKill(sendTimeout(mailer))
def status = proc.exitValue()
if( status != 0 ) {
consumer.join()
throw new MessagingException("Unable to send mail message\n $mailer exit status: $status\n reported error: $stdout")
}
}

private long sendTimeout(Mailer mailer) {
final timeout = mailer.config.sendMailTimeout as Duration
return timeout ? timeout.toMillis() : SEND_MAIL_TIMEOUT
}



}
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
/*
* Copyright 2020-2023, Seqera Labs
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
*/

package nextflow.mail

import javax.mail.internet.MimeMessage

import groovy.transform.CompileStatic
import groovy.util.logging.Slf4j

/**
* Implements a send mail provider based on Java Mail API
*
* @author Paolo Di Tommaso <[email protected]>
*/
@Slf4j
@CompileStatic
class JavaMailProvider implements MailProvider {
@Override
void send(MimeMessage message, Mailer mailer) {
if( !message.getAllRecipients() )
throw new IllegalArgumentException("Missing mail message recipient")

final transport = mailer.getSession().getTransport()
transport.connect(mailer.host, mailer.port as int, mailer.user, mailer.password)
log.trace("Connected to host=$mailer.host port=$mailer.port")
try {
transport.sendMessage(message, message.getAllRecipients())
}
finally {
transport.close()
}
}

@Override
String name() {
return 'javamail'
}

@Override
boolean textOnly() {
return false
}
}
37 changes: 37 additions & 0 deletions modules/nextflow/src/main/groovy/nextflow/mail/MailProvider.groovy
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
/*
* Copyright 2020-2023, Seqera Labs
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
*/

package nextflow.mail

import javax.mail.internet.MimeMessage

import org.pf4j.ExtensionPoint

/**
* Define a generic interface to send an email modelled as a Mime message object
*
* @author Paolo Di Tommaso <[email protected]>
*/
interface MailProvider extends ExtensionPoint {

void send(MimeMessage message, Mailer mailer)

String name()

boolean textOnly()

}
127 changes: 45 additions & 82 deletions modules/nextflow/src/main/groovy/nextflow/mail/Mailer.groovy
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
*/

package nextflow.mail

import javax.activation.DataHandler
import javax.activation.URLDataSource
import javax.mail.Message
Expand All @@ -34,7 +35,7 @@ import groovy.transform.CompileStatic
import groovy.transform.Memoized
import groovy.util.logging.Slf4j
import nextflow.io.LogOutputStream
import nextflow.util.Duration
import nextflow.plugin.Plugins
import org.jsoup.Jsoup
import org.jsoup.nodes.Document
import org.jsoup.parser.Parser
Expand All @@ -61,8 +62,6 @@ class Mailer {

private final static Pattern HTML_PATTERN = Pattern.compile("("+TAG_START+".*"+TAG_END+")|("+TAG_SELF_CLOSING+")|("+HTML_ENTITY+")", Pattern.DOTALL )

private long SEND_MAIL_TIMEOUT = 15_000

private static String DEF_CHARSET = Charset.defaultCharset().toString()

/**
Expand All @@ -87,6 +86,8 @@ class Mailer {
return this
}

Map getConfig() { config }

protected String getSysMailer() {
if( !fMailer )
fMailer = findSysMailer()
Expand Down Expand Up @@ -200,7 +201,7 @@ class Mailer {
getConfig('password')
}

protected getConfig(String name ) {
protected getConfig(String name) {
def key = "smtp.${name}"
def value = config.navigate(key)
if( !value ) {
Expand All @@ -210,58 +211,6 @@ class Mailer {
return value
}

/**
* Send a email message by using the Java API
*
* @param message A {@link MimeMessage} object representing the email to send
*/
protected void sendViaJavaMail(MimeMessage message) {
if( !message.getAllRecipients() )
throw new IllegalArgumentException("Missing mail message recipient")

final transport = getSession().getTransport()
transport.connect(host, port as int, user, password)
log.trace("Connected to host=$host port=$port")
try {
transport.sendMessage(message, message.getAllRecipients())
}
finally {
transport.close()
}
}

protected long getSendTimeout() {
def timeout = config.sendMailTimeout as Duration
return timeout ? timeout.toMillis() : SEND_MAIL_TIMEOUT
}

/**
* Send a email message by using system tool such as `sendmail` or `mail`
*
* @param message A {@link MimeMessage} object representing the email to send
*/
protected void sendViaSysMail(MimeMessage message) {
final mailer = getSysMailer()
final cmd = [mailer, '-t']
final proc = new ProcessBuilder()
.command(cmd)
.redirectErrorStream(true)
.start()
// pipe the message to the sendmail stdin
final stdout = new StringBuilder()
final stdin = proc.getOutputStream()
message.writeTo(stdin);
stdin.close() // <-- don't forget otherwise it hangs
// wait for the sending to complete
final consumer = proc.consumeProcessOutputStream(stdout)
proc.waitForOrKill(sendTimeout)
def status = proc.exitValue()
if( status != 0 ) {
consumer.join()
throw new MessagingException("Unable to send mail message\n $mailer exit status: $status\n reported error: $stdout")
}
}

/**
* @return A multipart mime message representing the mail message to send
*/
Expand Down Expand Up @@ -407,41 +356,55 @@ class Mailer {
guessHtml(str) ? 'text/html' : 'text/plain'
}

/**
* Send the mail given the provided config setting
*/
void send(Mail mail) {
log.trace "Mailer config: $config -- mail: $mail"
protected boolean detectAwsEnv() {
if( env.get('AWS_REGION') ) return true
if( env.get('AWS_DEFAULT_REGION') ) return true
return false
}

protected MailProvider provider() {
// load all providers
final providers = Plugins.getExtensions(MailProvider)
// find the AWS provider
final awsProvider = providers.find(it -> it.name()=='aws-ses')
// check if it can use the aws provider
if( env.get('NXF_ENABLE_AWS_SES')=='true' ) {
if( awsProvider )
return awsProvider
else
log.warn "Unable to load AWS Simple Email Service (SES) client"
}

// if the user provided required configuration
// send via Java Mail API
if( config.containsKey('smtp') ) {
log.trace "Mailer send via `javamail`"
def msg = createMimeMessage(mail)
sendViaJavaMail(msg)
return
return providers.find(it -> it.name()=='javamail')
}

final mailer = getSysMailer()
// otherwise fallback on system sendmail
if( mailer == 'sendmail' ) {
log.trace "Mailer send via `sendmail`"
def msg = createMimeMessage(mail)
sendViaSysMail(msg)
return
if( awsProvider && detectAwsEnv() ) {
return awsProvider
}

if( mailer == 'mail' ) {
log.trace "Mailer send via `mail`"
def msg = createTextMessage(mail)
sendViaSysMail(msg)
// detect the mailer type
final type = getSysMailer()
return providers.find(it -> it.name()==type)
}

/**
* Send the mail given the provided config setting
*/
void send(Mail mail) {
log.trace "Mailer config: $config -- mail: $mail"

final p = provider()
if( p != null ) {
log.debug "Sending mail via `${p.name()}`"
final msg = p.textOnly()
? createTextMessage(mail)
: createMimeMessage(mail)
p.send(msg, this)
return
}

String msg = (mailer
? "Unknown system mail tool: $mailer"
: "Cannot send email message -- Make sure you have installed `sendmail` or `mail` program or configure a mail SMTP server in the nextflow config file"
)
final msg = "Cannot send email message -- Make sure you have installed `sendmail` or `mail` program or configure a mail SMTP server in the nextflow config file"
throw new IllegalArgumentException(msg)
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
/*
* Copyright 2020-2023, Seqera Labs
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
*/

package nextflow.mail

/**
* Send a mail using the `sendmail` sys tool
*
* @author Paolo Di Tommaso <[email protected]>
*/
class SendMailProvider extends BaseMailProvider {

@Override
String name() {
return 'sendmail'
}

@Override
boolean textOnly() {
return false
}
}
Loading

0 comments on commit df85d44

Please sign in to comment.