Skip to content

Commit df85d44

Browse files
committed
Add support for AWS SES as mail sending provider
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]>
1 parent 1daebee commit df85d44

File tree

12 files changed

+369
-88
lines changed

12 files changed

+369
-88
lines changed
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
/*
2+
* Copyright 2020-2023, Seqera Labs
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*
16+
*/
17+
18+
package nextflow.mail
19+
20+
import javax.mail.MessagingException
21+
import javax.mail.internet.MimeMessage
22+
23+
import groovy.transform.CompileStatic
24+
import nextflow.util.Duration
25+
26+
/**
27+
* Base class or sending an email via sys command `mail` or `sendmail`
28+
*
29+
* @author Paolo Di Tommaso <[email protected]>
30+
*/
31+
@CompileStatic
32+
abstract class BaseMailProvider implements MailProvider {
33+
34+
private long SEND_MAIL_TIMEOUT = 15_000
35+
36+
/**
37+
* Send a email message by using system tool such as `sendmail` or `mail`
38+
*
39+
* @param message A {@link MimeMessage} object representing the email to send
40+
*/
41+
void send(MimeMessage message, Mailer mailer) {
42+
final cmd = [name(), '-t']
43+
final proc = new ProcessBuilder()
44+
.command(cmd)
45+
.redirectErrorStream(true)
46+
.start()
47+
// pipe the message to the sendmail stdin
48+
final stdout = new StringBuilder()
49+
final stdin = proc.getOutputStream()
50+
message.writeTo(stdin);
51+
stdin.close() // <-- don't forget otherwise it hangs
52+
// wait for the sending to complete
53+
final consumer = proc.consumeProcessOutputStream(stdout)
54+
proc.waitForOrKill(sendTimeout(mailer))
55+
def status = proc.exitValue()
56+
if( status != 0 ) {
57+
consumer.join()
58+
throw new MessagingException("Unable to send mail message\n $mailer exit status: $status\n reported error: $stdout")
59+
}
60+
}
61+
62+
private long sendTimeout(Mailer mailer) {
63+
final timeout = mailer.config.sendMailTimeout as Duration
64+
return timeout ? timeout.toMillis() : SEND_MAIL_TIMEOUT
65+
}
66+
67+
68+
69+
}
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
/*
2+
* Copyright 2020-2023, Seqera Labs
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*
16+
*/
17+
18+
package nextflow.mail
19+
20+
import javax.mail.internet.MimeMessage
21+
22+
import groovy.transform.CompileStatic
23+
import groovy.util.logging.Slf4j
24+
25+
/**
26+
* Implements a send mail provider based on Java Mail API
27+
*
28+
* @author Paolo Di Tommaso <[email protected]>
29+
*/
30+
@Slf4j
31+
@CompileStatic
32+
class JavaMailProvider implements MailProvider {
33+
@Override
34+
void send(MimeMessage message, Mailer mailer) {
35+
if( !message.getAllRecipients() )
36+
throw new IllegalArgumentException("Missing mail message recipient")
37+
38+
final transport = mailer.getSession().getTransport()
39+
transport.connect(mailer.host, mailer.port as int, mailer.user, mailer.password)
40+
log.trace("Connected to host=$mailer.host port=$mailer.port")
41+
try {
42+
transport.sendMessage(message, message.getAllRecipients())
43+
}
44+
finally {
45+
transport.close()
46+
}
47+
}
48+
49+
@Override
50+
String name() {
51+
return 'javamail'
52+
}
53+
54+
@Override
55+
boolean textOnly() {
56+
return false
57+
}
58+
}
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
/*
2+
* Copyright 2020-2023, Seqera Labs
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*
16+
*/
17+
18+
package nextflow.mail
19+
20+
import javax.mail.internet.MimeMessage
21+
22+
import org.pf4j.ExtensionPoint
23+
24+
/**
25+
* Define a generic interface to send an email modelled as a Mime message object
26+
*
27+
* @author Paolo Di Tommaso <[email protected]>
28+
*/
29+
interface MailProvider extends ExtensionPoint {
30+
31+
void send(MimeMessage message, Mailer mailer)
32+
33+
String name()
34+
35+
boolean textOnly()
36+
37+
}

modules/nextflow/src/main/groovy/nextflow/mail/Mailer.groovy

Lines changed: 45 additions & 82 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
*/
1616

1717
package nextflow.mail
18+
1819
import javax.activation.DataHandler
1920
import javax.activation.URLDataSource
2021
import javax.mail.Message
@@ -34,7 +35,7 @@ import groovy.transform.CompileStatic
3435
import groovy.transform.Memoized
3536
import groovy.util.logging.Slf4j
3637
import nextflow.io.LogOutputStream
37-
import nextflow.util.Duration
38+
import nextflow.plugin.Plugins
3839
import org.jsoup.Jsoup
3940
import org.jsoup.nodes.Document
4041
import org.jsoup.parser.Parser
@@ -61,8 +62,6 @@ class Mailer {
6162

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

64-
private long SEND_MAIL_TIMEOUT = 15_000
65-
6665
private static String DEF_CHARSET = Charset.defaultCharset().toString()
6766

6867
/**
@@ -87,6 +86,8 @@ class Mailer {
8786
return this
8887
}
8988

89+
Map getConfig() { config }
90+
9091
protected String getSysMailer() {
9192
if( !fMailer )
9293
fMailer = findSysMailer()
@@ -200,7 +201,7 @@ class Mailer {
200201
getConfig('password')
201202
}
202203

203-
protected getConfig(String name ) {
204+
protected getConfig(String name) {
204205
def key = "smtp.${name}"
205206
def value = config.navigate(key)
206207
if( !value ) {
@@ -210,58 +211,6 @@ class Mailer {
210211
return value
211212
}
212213

213-
/**
214-
* Send a email message by using the Java API
215-
*
216-
* @param message A {@link MimeMessage} object representing the email to send
217-
*/
218-
protected void sendViaJavaMail(MimeMessage message) {
219-
if( !message.getAllRecipients() )
220-
throw new IllegalArgumentException("Missing mail message recipient")
221-
222-
final transport = getSession().getTransport()
223-
transport.connect(host, port as int, user, password)
224-
log.trace("Connected to host=$host port=$port")
225-
try {
226-
transport.sendMessage(message, message.getAllRecipients())
227-
}
228-
finally {
229-
transport.close()
230-
}
231-
}
232-
233-
protected long getSendTimeout() {
234-
def timeout = config.sendMailTimeout as Duration
235-
return timeout ? timeout.toMillis() : SEND_MAIL_TIMEOUT
236-
}
237-
238-
/**
239-
* Send a email message by using system tool such as `sendmail` or `mail`
240-
*
241-
* @param message A {@link MimeMessage} object representing the email to send
242-
*/
243-
protected void sendViaSysMail(MimeMessage message) {
244-
final mailer = getSysMailer()
245-
final cmd = [mailer, '-t']
246-
final proc = new ProcessBuilder()
247-
.command(cmd)
248-
.redirectErrorStream(true)
249-
.start()
250-
// pipe the message to the sendmail stdin
251-
final stdout = new StringBuilder()
252-
final stdin = proc.getOutputStream()
253-
message.writeTo(stdin);
254-
stdin.close() // <-- don't forget otherwise it hangs
255-
// wait for the sending to complete
256-
final consumer = proc.consumeProcessOutputStream(stdout)
257-
proc.waitForOrKill(sendTimeout)
258-
def status = proc.exitValue()
259-
if( status != 0 ) {
260-
consumer.join()
261-
throw new MessagingException("Unable to send mail message\n $mailer exit status: $status\n reported error: $stdout")
262-
}
263-
}
264-
265214
/**
266215
* @return A multipart mime message representing the mail message to send
267216
*/
@@ -407,41 +356,55 @@ class Mailer {
407356
guessHtml(str) ? 'text/html' : 'text/plain'
408357
}
409358

410-
/**
411-
* Send the mail given the provided config setting
412-
*/
413-
void send(Mail mail) {
414-
log.trace "Mailer config: $config -- mail: $mail"
359+
protected boolean detectAwsEnv() {
360+
if( env.get('AWS_REGION') ) return true
361+
if( env.get('AWS_DEFAULT_REGION') ) return true
362+
return false
363+
}
364+
365+
protected MailProvider provider() {
366+
// load all providers
367+
final providers = Plugins.getExtensions(MailProvider)
368+
// find the AWS provider
369+
final awsProvider = providers.find(it -> it.name()=='aws-ses')
370+
// check if it can use the aws provider
371+
if( env.get('NXF_ENABLE_AWS_SES')=='true' ) {
372+
if( awsProvider )
373+
return awsProvider
374+
else
375+
log.warn "Unable to load AWS Simple Email Service (SES) client"
376+
}
415377

416-
// if the user provided required configuration
417-
// send via Java Mail API
418378
if( config.containsKey('smtp') ) {
419-
log.trace "Mailer send via `javamail`"
420-
def msg = createMimeMessage(mail)
421-
sendViaJavaMail(msg)
422-
return
379+
return providers.find(it -> it.name()=='javamail')
423380
}
424381

425-
final mailer = getSysMailer()
426-
// otherwise fallback on system sendmail
427-
if( mailer == 'sendmail' ) {
428-
log.trace "Mailer send via `sendmail`"
429-
def msg = createMimeMessage(mail)
430-
sendViaSysMail(msg)
431-
return
382+
if( awsProvider && detectAwsEnv() ) {
383+
return awsProvider
432384
}
433385

434-
if( mailer == 'mail' ) {
435-
log.trace "Mailer send via `mail`"
436-
def msg = createTextMessage(mail)
437-
sendViaSysMail(msg)
386+
// detect the mailer type
387+
final type = getSysMailer()
388+
return providers.find(it -> it.name()==type)
389+
}
390+
391+
/**
392+
* Send the mail given the provided config setting
393+
*/
394+
void send(Mail mail) {
395+
log.trace "Mailer config: $config -- mail: $mail"
396+
397+
final p = provider()
398+
if( p != null ) {
399+
log.debug "Sending mail via `${p.name()}`"
400+
final msg = p.textOnly()
401+
? createTextMessage(mail)
402+
: createMimeMessage(mail)
403+
p.send(msg, this)
438404
return
439405
}
440406

441-
String msg = (mailer
442-
? "Unknown system mail tool: $mailer"
443-
: "Cannot send email message -- Make sure you have installed `sendmail` or `mail` program or configure a mail SMTP server in the nextflow config file"
444-
)
407+
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"
445408
throw new IllegalArgumentException(msg)
446409
}
447410

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
/*
2+
* Copyright 2020-2023, Seqera Labs
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*
16+
*/
17+
18+
package nextflow.mail
19+
20+
/**
21+
* Send a mail using the `sendmail` sys tool
22+
*
23+
* @author Paolo Di Tommaso <[email protected]>
24+
*/
25+
class SendMailProvider extends BaseMailProvider {
26+
27+
@Override
28+
String name() {
29+
return 'sendmail'
30+
}
31+
32+
@Override
33+
boolean textOnly() {
34+
return false
35+
}
36+
}

0 commit comments

Comments
 (0)