Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions .devcontainer/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,9 @@ ENV HOME="/root"
# --------------------------------------
# Git
# --------------------------------------
# Need to add the devcontainer workspace folder as a safe directory to enable git
# Need to add the devcontainer workspace folder as a safe directory to enable git
# version control system to be enabled in the containers file system.
RUN git config --global --add safe.directory "/workspaces/plugin-template"
RUN git config --global --add safe.directory "/workspaces/plugin-email"
# --------------------------------------

# --------------------------------------
Expand Down Expand Up @@ -53,11 +53,11 @@ ENV PATH="$PATH:$JAVA_HOME/bin"
# Will load a custom configuration file for Micronaut
ENV MICRONAUT_ENVIRONMENTS=local,override
# Sets the path where you save plugins as Jar and is loaded during the startup process
ENV KESTRA_PLUGINS_PATH="/workspaces/plugin-template/local/plugins"
ENV KESTRA_PLUGINS_PATH="/workspaces/plugin-email/local/plugins"
# --------------------------------------

# --------------------------------------
# SSH
# SSH
# --------------------------------------
RUN mkdir -p ~/.ssh
RUN touch ~/.ssh/config
Expand Down
4 changes: 2 additions & 2 deletions .devcontainer/devcontainer.json
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
{
"name": "plugin-template",
"name": "plugin-email",
"build": {
"context": ".",
"dockerfile": "Dockerfile"
},
"workspaceFolder": "/workspaces/plugin-template",
"workspaceFolder": "/workspaces/plugin-email",
"forwardPorts": [8080],
"customizations": {
"vscode": {
Expand Down
14 changes: 11 additions & 3 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ java {
}

group = "io.kestra.plugin"
description = 'Plugin template for Kestra'
description = 'Plugin Email for Kestra'

tasks.withType(JavaCompile).configureEach {
options.encoding = "UTF-8"
Expand All @@ -49,6 +49,10 @@ dependencies {
annotationProcessor group: "io.kestra", name: "processor", version: kestraVersion
compileOnly group: "io.kestra", name: "core", version: kestraVersion
compileOnly group: "io.kestra", name: "script", version: kestraVersion

// libs
api 'org.simplejavamail:simple-java-mail:8.12.6'
api 'org.eclipse.angus:jakarta.mail:2.0.5'
}


Expand Down Expand Up @@ -91,11 +95,15 @@ dependencies {
testImplementation group: "io.kestra", name: "repository-memory", version: kestraVersion
testImplementation group: "io.kestra", name: "runner-memory", version: kestraVersion
testImplementation group: "io.kestra", name: "storage-local", version: kestraVersion
testImplementation group: "io.kestra", name: "scheduler", version: kestraVersion
testImplementation group: "io.kestra", name: "worker", version: kestraVersion

// test
testImplementation "org.junit.jupiter:junit-jupiter-engine"
testImplementation "org.hamcrest:hamcrest"
testImplementation "org.hamcrest:hamcrest-library"

testImplementation group: 'com.icegreen', name: 'greenmail-junit5', version: '2.1.7'
}

/**********************************************************************************************************************\
Expand Down Expand Up @@ -175,8 +183,8 @@ jar {
manifest {
attributes(
"X-Kestra-Name": project.name,
"X-Kestra-Title": "Template",
"X-Kestra-Group": project.group + ".templates",
"X-Kestra-Title": "Email",
"X-Kestra-Group": project.group + ".email",
"X-Kestra-Description": project.description,
"X-Kestra-Version": project.version
)
Expand Down
2 changes: 1 addition & 1 deletion gradle.properties
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
version=1.1.0-SNAPSHOT
kestraVersion=1.0.0
kestraVersion=1.1.11
2 changes: 1 addition & 1 deletion settings.gradle
Original file line number Diff line number Diff line change
@@ -1 +1 @@
rootProject.name = 'plugin-template'
rootProject.name = 'plugin-email'
74 changes: 74 additions & 0 deletions src/main/java/io/kestra/plugin/email/AbstractMailTrigger.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
package io.kestra.plugin.email;

import io.kestra.core.models.property.Property;
import io.kestra.core.models.triggers.AbstractTrigger;
import io.kestra.core.runners.RunContext;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotNull;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.experimental.SuperBuilder;

import java.time.Duration;


@SuperBuilder
@Getter
@NoArgsConstructor
public abstract class AbstractMailTrigger extends AbstractTrigger {

public Duration getInterval(){
return Duration.ofSeconds(60);
}
@Schema(title = "Mail server protocol", description = "The protocol to use for connecting to the mail server")
@Builder.Default
protected final Property<MailService.Protocol> protocol = Property.ofValue(MailService.Protocol.IMAP);

@Schema(title = "Mail server host", description = "The hostname or IP address of the mail server")
@NotNull
protected Property<String> host;

@Schema(title = "Mail server port", description = "The port number of the mail server. Defaults: IMAP=993 (SSL), 143 (non-SSL); POP3=995 (SSL), 110 (non-SSL)")
protected Property<Integer> port;

@Schema(title = "Username", description = "The username for authentication")
@NotNull
protected Property<String> username;

@Schema(title = "Password", description = "The password for authentication")
@NotNull
protected Property<String> password;

@Schema(title = "Mail folder", description = "The mail folder to monitor (IMAP only)")
@Builder.Default
protected final Property<String> folder = Property.ofValue("INBOX");

@Schema(title = "Use SSL", description = "Whether to use SSL/TLS encryption")
@Builder.Default
protected final Property<Boolean> ssl = Property.ofValue(true);

@Schema(title = "Trust all certificates", description = "Whether to trust all SSL certificates (use with caution)")
@Builder.Default
protected final Property<Boolean> trustAllCertificates = Property.ofValue(false);

@Schema(title = "Check interval", description = "How frequently to check for new emails")
@Builder.Default
protected final Property<Duration> interval = Property.ofValue(Duration.ofSeconds(60));

protected MailService.MailConfiguration renderMailConfiguration(RunContext runContext) throws Exception {
String rProtocol = String.valueOf(runContext.render(this.protocol).as(MailService.Protocol.class).orElseThrow());
String rHost = runContext.render(this.host).as(String.class).orElseThrow();
String rUsername = runContext.render(this.username).as(String.class).orElseThrow();
String rPassword = runContext.render(this.password).as(String.class).orElseThrow();
String rFolder = runContext.render(this.folder).as(String.class).orElse("INBOX");
Boolean rSsl = runContext.render(this.ssl).as(Boolean.class).orElse(true);
Boolean rTrustAllCertificates = runContext.render(this.trustAllCertificates).as(Boolean.class).orElse(false);
Duration rInterval = runContext.render(this.interval).as(Duration.class).orElse(getInterval());

Integer rPort = runContext.render(this.port).as(Integer.class)
.orElse(MailService.getDefaultPort(MailService.Protocol.valueOf(rProtocol), rSsl));

return new MailService.MailConfiguration(rProtocol, rHost, rPort, rUsername, rPassword, rFolder, rSsl, rTrustAllCertificates, rInterval);
}
}
81 changes: 81 additions & 0 deletions src/main/java/io/kestra/plugin/email/MailExecution.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
package io.kestra.plugin.email;

import io.kestra.core.models.annotations.Example;
import io.kestra.core.models.annotations.Plugin;
import io.kestra.core.models.property.Property;
import io.kestra.core.models.tasks.VoidOutput;
import io.kestra.core.plugins.notifications.ExecutionInterface;
import io.kestra.core.plugins.notifications.ExecutionService;
import io.kestra.core.runners.RunContext;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.*;
import lombok.experimental.SuperBuilder;

import java.util.Map;

@SuperBuilder
@ToString
@EqualsAndHashCode
@Getter
@NoArgsConstructor
@Schema(
title = "Send an email with the execution information.",
description = "The message will include a link to the execution page in the UI along with the execution ID, namespace, flow name, the start date, duration, and the final status of the execution. If the task failed, then the task that led to the failure is specified.\n\n" +
"Use this notification task only in a flow that has a [Flow trigger](https://kestra.io/docs/administrator-guide/monitoring#alerting), as shown in this example. Don't use this notification task in `errors` tasks. Instead, for `errors` tasks, use the [MailSend](https://kestra.io/plugins/plugin-notifications/tasks/mail/io.kestra.plugin.email.mailsend) task."
)
@Plugin(
examples = {
@Example(
title = "Send an email notification on a failed flow execution",
full = true,
code = """
id: failure_alert
namespace: company.team

tasks:
- id: send_alert
type: io.kestra.plugin.email.MailExecution
to: hello@kestra.io
from: hello@kestra.io
subject: "The workflow execution {{trigger.executionId}} failed for the flow {{trigger.flowId}} in the namespace {{trigger.namespace}}"
host: mail.privateemail.com
port: 465
username: "{{ secret('EMAIL_USERNAME') }}"
password: "{{ secret('EMAIL_PASSWORD') }}"
executionId: "{{ trigger.executionId }}"

triggers:
- id: failed_prod_workflows
type: io.kestra.plugin.core.trigger.Flow
conditions:
- type: io.kestra.plugin.core.condition.ExecutionStatus
in:
- FAILED
- WARNING
- type: io.kestra.plugin.core.condition.ExecutionNamespace
namespace: prod
prefix: true
"""
)
}
)
public class MailExecution extends MailTemplate implements ExecutionInterface {
@Builder.Default
private final Property<String> executionId = Property.ofExpression("{{ execution.id }}");
private Property<Map<String, Object>> customFields;
private Property<String> customMessage;

@Schema(
hidden = true
)
protected Property<String> htmlTextContent;

@Override
public VoidOutput run(RunContext runContext) throws Exception {
this.templateUri = Property.ofValue("mail-template.hbs.peb");
this.textTemplateUri = Property.ofValue("text-template.hbs.peb");
this.templateRenderMap = Property.ofValue(ExecutionService.executionMap(runContext, this));

return super.run(runContext);
}
}
111 changes: 111 additions & 0 deletions src/main/java/io/kestra/plugin/email/MailReceivedTrigger.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
package io.kestra.plugin.email;

import io.kestra.core.models.annotations.Example;
import io.kestra.core.models.annotations.Plugin;
import io.kestra.core.models.conditions.ConditionContext;
import io.kestra.core.models.executions.Execution;
import io.kestra.core.models.triggers.PollingTriggerInterface;
import io.kestra.core.models.triggers.TriggerContext;
import io.kestra.core.models.triggers.TriggerOutput;
import io.kestra.core.models.triggers.TriggerService;
import io.kestra.core.runners.RunContext;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.ToString;
import lombok.experimental.SuperBuilder;

import java.time.Duration;
import java.time.ZonedDateTime;
import java.util.Comparator;
import java.util.List;
import java.util.Optional;

@SuperBuilder
@ToString
@EqualsAndHashCode
@Getter
@NoArgsConstructor
@Schema(
title = "Trigger on new email messages.",
description = "Monitor a mailbox for new emails via IMAP or POP3 protocols."
)
@Plugin(
examples = {
@Example(
title = "Monitor Gmail inbox for new emails",
full = true,
code = """
id: email_monitor
namespace: company.team

tasks:
- id: process_email
type: io.kestra.core.tasks.log.Log
message: |
New email received:
Subject: {{ trigger.latestEmail.subject }}
From: {{ trigger.latestEmail.from }}
Date: {{ trigger.latestEmail.date }}

triggers:
- id: gmail_inbox_trigger
type: io.kestra.plugin.email.MailReceivedTrigger
protocol: IMAP
host: imap.gmail.com
port: 993
username: "{{ secret('GMAIL_USERNAME') }}"
password: "{{ secret('GMAIL_PASSWORD') }}"
folder: INBOX
interval: PT30S
ssl: true
"""
)
}
)
public class MailReceivedTrigger extends AbstractMailTrigger
implements PollingTriggerInterface, TriggerOutput<MailService.Output> {

@Override
public Optional<Execution> evaluate(ConditionContext conditionContext, TriggerContext context) throws Exception {
RunContext runContext = conditionContext.getRunContext();
MailService.MailConfiguration mailConfig = renderMailConfiguration(runContext);

try {
ZonedDateTime lastCheckTime = getLastCheckTime(context,mailConfig.interval);
List<MailService.EmailData> newEmails = MailService.fetchNewEmails(runContext, mailConfig.protocol,
mailConfig.host, mailConfig.port,
mailConfig.username, mailConfig.password, mailConfig.folder, mailConfig.ssl,
mailConfig.trustAllCertificates, lastCheckTime);

if (newEmails.isEmpty()) {
return Optional.empty();
}

MailService.EmailData latest = newEmails.stream()
.max(Comparator.comparing(MailService.EmailData::getDate))
.orElse(newEmails.getFirst());

MailService.Output output = MailService.Output.builder()
.latestEmail(latest)
.total(newEmails.size())
.emails(newEmails)
.build();

Execution execution = TriggerService.generateExecution(this, conditionContext, context, output);
return Optional.of(execution);

} catch (Exception e) {
runContext.logger().error("Error checking for new emails", e);
throw new RuntimeException("Failed to fetch emails: " + e.getMessage(), e);
}
}

private ZonedDateTime getLastCheckTime(TriggerContext context,Duration interval){
if(context.getNextExecutionDate()==null){
return ZonedDateTime.now().minus(getInterval());
}
return context.getNextExecutionDate().minus(interval);
}
}
Loading
Loading