Verificando autenticación…

Skip to main content

Email ACL Template

Purpose and Scope

This project acts as an Email ACL (Access Control Layer). Its main objective is to receive email sending requests through Google Cloud Pub/Sub messages and process them for delivery using Amazon Simple Email Service (SES).

Problem It Solves: It provides a centralized, decoupled interface for sending emails, allowing other services to delegate this functionality without directly integrating the Amazon SES SDK or managing AWS credentials in a distributed manner.

General Scope:

  • Reception of messages in JSON format from a Google Cloud Pub/Sub subscription.
  • Parsing of the message to extract recipients, subject, email body (HTML supported), and attachments.
  • Dynamic construction of the email sender based on requesting application metadata and the environment.
  • Conversion and handling of attachments (Base64-encoded).
  • Integration with Amazon SES for actual email delivery.
  • Environment-specific configuration handling (local, dev, intg, prod).

Out of Scope:

  • Does not expose a public REST API for sending emails (interaction is via Pub/Sub).
  • Does not manage complex email templates (the email body is received directly).
  • Does not implement advanced retry logic beyond what Pub/Sub may offer.

Architecture

The project follows a hexagonal architecture (ports and adapters), seeking good decoupling between business logic (domain) and infrastructure components (integrations with external services).

Main Flow:

  1. A message is published to a Google Cloud Pub/Sub topic by a client service.
  2. The PubSubReceiver (input adapter) in this project is subscribed to that topic (through a specific subscription) and receives the message.
  3. The message (JSON payload) is processed by PubSubService (application/domain logic).
  4. PubSubService transforms the payload into an EmailDetails object, determines the sender (from email), and prepares the details for sending.
  5. If there are attachments, they are decoded from Base64 and prepared.
  6. EmailAmazonService and EmailAmazonRawService (output adapters) use the Amazon SES client to build and send the email.
  7. Credentials for AWS SES (AWS_ACCESS_KEY, AWS_SECRET_KEY) are managed securely, typically through secrets in GCP Secret Manager injected as environment variables in the GKE deployment.

Project Structure (simplified):

com.latam.ltmds.email.acl
├── application
│ └── request/ # DTOs such as EmailDetails, Attachment
├── domain
│ ├── service/ # Main business logic
│ │ ├── email/ # Email-related services (SES interface)
│ │ └── pubsub/ # Pub/Sub-related services (message processing)
│ └── exception/ # Custom exceptions
└── infrastructure
├── config/ # Spring configuration, Beans
├── pubsub/ # Adapter to receive Pub/Sub messages
└── ses/ # Adapter to send emails via Amazon SES

Technologies and Dependencies

  • Language: Java 21
  • Framework: Spring Boot 3.4.5
  • Build Tool: Gradle 8.12.1
  • Asynchronous Messaging:
    • Google Cloud Pub/Sub (Receiver): com.google.cloud:spring-cloud-gcp-starter-pubsub
    • Amazon Simple Email Service (SES) (Sender): software.amazon.awssdk:ses
  • Embedded Server: Tomcat (default in Spring Boot Starter Web)
  • Key Libraries:
    • spring-boot-starter-web: For basic web functionality and Actuator.
    • spring-boot-starter-integration: For Pub/Sub integration.
    • spring-boot-starter-validation: For validations.
    • jakarta.mail: API for building emails.
    • org.projectlombok:lombok: To reduce boilerplate code.
    • com.latam:latam-logging-library: For standardized logging.
    • com.google.code.gson:gson: For JSON manipulation (if needed in addition to Jackson).
    • io.netty: Transitive dependency, managed to resolve vulnerabilities.
  • Testing:
    • JUnit 5
    • Mockito
  • Observability:
    • Spring Boot Actuator
    • Integration with Google Cloud Logging & Trace (if enabled)
  • Licenses: See LibrariesLicenses.txt for full details. The project itself is Proprietary and confidential per LICENSE.

Local Setup

Prerequisites

  • JDK 21.
  • Gradle (the gradlew wrapper is included in the project).
  • An IDE such as IntelliJ IDEA or Eclipse.
  • Access to LATAM JFrog Artifactory for corporate dependencies.
  • AWS Credentials: AWS_ACCESS_KEY and AWS_SECRET_KEY with permissions for Amazon SES.
  • GCP Credentials: A service account credentials file (GOOGLE_APPLICATION_CREDENTIALS) with permissions for Google Cloud Pub/Sub (read from subscription) and, if used, Secret Manager.
  • A created Pub/Sub subscription and a topic to publish test messages to.

Setup Steps

  1. Clone the repository: Follow these steps

  2. Configure Artifactory credentials: Create a gradle.properties file at the project root (this file is in .gitignore) with your credentials:

    ARTIFACTORY_BASE_URL=https://artifactoryrepo1.appslatam.com/artifactory/
    ARTIFACTORY_GRADLE_CORP_REPO=corp-libs-gradle-release
    ARTIFACTORY_USERNAME=<tu_usuario_artifactory>
    ARTIFACTORY_PASSWORD=<tu_api_key_artifactory>
  3. Configure environment variables for AWS and GCP locally: You can configure these variables on your system or in your IDE run configuration.

    • AWS_ACCESS_KEY=<tu_aws_access_key>
    • AWS_SECRET_KEY=<tu_aws_secret_key>
    • GOOGLE_APPLICATION_CREDENTIALS=/ruta/a/tu/archivo-credenciales-gcp.json
    • PROJECT_ID=<tu_gcp_project_id>
    • SUBSCRIPTION-NAME=<tu_nombre_de_suscripcion_pubsub> (This value is used in application.yaml; it may need adjustment in application-local.yaml or specific profiles)
  4. Adjust application-local.yaml (if necessary): Verify configurations in src/main/resources/application-local.yaml and src/main/resources/application.yaml for the Pub/Sub subscription and other specific parameters. For example, in application.yaml:

    gcp:
    subscription:
    name: ${{ SUBSCRIPTION-NAME }} # Ensure this variable resolves or configure directly for local
  5. Build the project:

    ./gradlew clean build
  6. Run the application:

    ./gradlew bootRun

    The application will start and begin listening for messages on the configured Pub/Sub subscription.

Send a Test Message (via GCP Console or gcloud CLI)

Publish a JSON message to the Pub/Sub topic associated with your configured subscription. Message format:

{
"toEmail": "destinatario1@example.com,destinatario2@example.com",
"subject": "Asunto del correo de prueba",
"body": "<h1>Hola!</h1><p>Este es el cuerpo <b>HTML</b> del correo de prueba.</p>",
"itelement": "MI_ITELEMENT",
"application": "MI_APLICACION",
"sender": "opcional-remitente-explicito@example.com",
"attachments": [
{
"byteArray": "BASE64_ENCODED_STRING_OF_FILE_BYTES_1",
"fileName": "archivo1.pdf"
},
{
"byteArray": "BASE64_ENCODED_STRING_OF_FILE_BYTES_2",
"fileName": "imagen.png"
}
]
}
  • toEmail: Comma-separated list of email addresses.
  • body: Can be plain text or HTML.
  • itelement, application: Used to build the sender if sender is not provided.
  • sender (optional): If provided, this sender is used directly. Otherwise, it is built as <application>-<itelement>@mails-noprod.appslatam.com (or @mails.appslatam.com for prod).
  • attachments (optional): List of objects, each with byteArray (file content in Base64) and fileName.

Deployment

The project is configured to be deployed on Google Kubernetes Engine (GKE) using the LATAM corporate pipeline.

  • Jenkins Pipeline: The Jenkinsfile defines the pipeline to use:
    pipelineLatam("gke-krane-java21")
  • Kubernetes Configuration: Located in the deploy/gke/ folder. The main file is deployment.yaml.erb, a template processed during deployment.
  • Environment Variables per Environment: Files in deploy/env/ (e.g. dev.yaml, intg.yaml, prod.yaml) are used to specify dynamic environment variables for each environment.
  • Dockerfile: Located at the project root, defines the Docker image to build. Uses a LATAM JRE 21 base image. FROM <%= redhat_ubi_8_jre_21 %> (or similar latam-jre21).
  • Key Environment Variables in Deployment:
    • ENVIRONMENT: (dev, intg, prod)
    • AWS_ACCESS_KEY: AWS access key (obtained from Secret Manager).
    • AWS_SECRET_KEY: AWS secret key (obtained from Secret Manager).
    • PROJECT_ID: GCP project ID.
    • SUBSCRIPTION-NAME: Full Pub/Sub subscription name (e.g. projects/mi-proyecto/subscriptions/mi-subscripcion).
    • Other configMap configurations in deployment.yaml.erb.

API Endpoints

This service does not expose a traditional REST API for its main functionality, as it operates by consuming Google Cloud Pub/Sub messages.

Main Interaction via Pub/Sub:

  • Mechanism: The service listens to a Pub/Sub subscription.
  • Message Format: JSON (see example in "Local Setup" section).

Actuator Endpoints (Management and Health): These endpoints are exposed by Spring Boot Actuator and are configured in deployment.yaml.erb and application.yaml. The contextPath is /api/email/ses.

MethodEndpointDescription
GET/api/email/ses/actuator/healthOverall application health status.
GET/api/email/ses/actuator/health/readinessKubernetes Readiness probe.
GET/api/email/ses/actuator/health/livenessKubernetes Liveness probe.
GET/api/email/ses/actuator/infoApplication information (build, git).
GET/api/email/ses/actuator/loggersView and modify log levels.
GET/api/email/ses/actuator/metricsApplication metrics (JVM, HTTP, etc.).

There is no explicit swagger.yaml for business endpoints, but if springdoc-openapi is active, /v3/api-docs may be available for Actuator endpoints.

Specific Components

Google Cloud Pub/Sub

  • Purpose: Receive email sending requests asynchronously.
  • Configuration: In src/main/resources/application.yaml:
    spring:
    cloud:
    gcp:
    project-id: ${PROJECT_ID} # Environment variable
    # ...
    gcp:
    subscription:
    name: ${{ SUBSCRIPTION-NAME }} # Environment variable, e.g. projects/mi-proyecto/subscriptions/mi-suscripcion-email
    Authentication is typically handled via the GOOGLE_APPLICATION_CREDENTIALS environment variable (locally) or the service account assigned to the Kubernetes pod (on GKE).
  • Main Implementation:
    • com.latam.ltmds.email.acl.infrastructure.pubsub.PubSubReceiver: Contains the @ServiceActivator that listens to messages from PubSubInboundChannelAdapter.
    • com.latam.ltmds.email.acl.domain.service.pubsub.PubSubService: Processes the received message logic, transforms the payload, and calls the email service.
  • Disable (if necessary):
    1. Remove the com.google.cloud:spring-cloud-gcp-starter-pubsub dependency from gradle/dependencies.gradle.
    2. Remove or comment out Pub/Sub-related classes (PubSubReceiver, PubSubService) and their configuration.
    3. Remove gcp.subscription.name and gcp.project-id configurations from application.yaml.

Amazon Simple Email Service (SES)

  • Purpose: Actually send emails.

  • Configuration: In src/main/resources/application.yaml:

    aws:
    region:
    static: us-east-1 # AWS SES region
    auto: false
    stack:
    auto: false
    credentials:
    access-key: ${AWS_ACCESS_KEY:aws_key} # Environment variable
    secret-key: ${AWS_SECRET_KEY:aws_secret_key} # Environment variable

    mail:
    sender:
    non-prod: "@mails-noprod.appslatam.com" # Domain for senders in non-production
    prod: "@mails.appslatam.com" # Domain for senders in production

    Credentials AWS_ACCESS_KEY and AWS_SECRET_KEY must be configured as environment variables or secrets (on GKE, they are obtained from GCP Secret Manager).

  • Main Implementation:

    • com.latam.ltmds.email.acl.infrastructure.ses.AmazonSesConfig: Configures the AWS SDK SesClient.
    • com.latam.ltmds.email.acl.domain.service.email.EmailAmazonService: Facade service for sending emails.
    • com.latam.ltmds.email.acl.domain.service.email.EmailAmazonRawService: Logic to build the MIME message (including HTML body and attachments) and send it using SES SendRawEmail.
  • Disable (if necessary):

    1. Remove the software.amazon.awssdk:ses dependency from gradle/dependencies.gradle.
    2. Remove or comment out SES-related classes (AmazonSesConfig, EmailAmazonService, EmailAmazonRawService).
    3. Remove aws.* and mail.sender.* configurations from application.yaml.

Testing

The project uses JUnit 5 and Mockito for unit tests and lightweight integration tests.

  • Run Tests:

    ./gradlew test

    This will run all tests in src/test/java.

  • Test Configuration: Located in gradle/test.gradle. Includes Jacoco configuration for coverage report generation.

    jacoco {
    toolVersion = "0.8.12"
    reportsDirectory = layout.buildDirectory.dir('reports/jacoco/test/html/')
    }

    test {
    systemProperty 'spring.profiles.active', 'test' // Activates the 'test' profile for tests
    useJUnitPlatform()
    finalizedBy jacocoTestReport
    }

    jacocoTestReport {
    // ... report configuration ...
    }
  • Coverage Reports: After running ./gradlew test, Jacoco HTML reports will be found at build/reports/jacoco/test/html/index.html.

  • Test Configuration Files: src/main/resources/application-test.yaml may contain specific configurations for the test environment.

Security Considerations

  • Credential Management:
    • AWS credentials (AWS_ACCESS_KEY, AWS_SECRET_KEY) must NEVER be hardcoded in code or in versioned configuration files. They must be injected as environment variables at runtime. On GKE, this is managed through GCP Secret Manager.
    • For GCP, authentication is preferably performed via the service account associated with the GKE pod. Locally, the file referenced by GOOGLE_APPLICATION_CREDENTIALS is used.
  • License: The project has a proprietary license (LICENSE - "Proprietary and confidential"). Ensure you comply with its terms.
  • Dependencies: Keep dependencies up to date to mitigate known vulnerabilities. Regularly review SonarQube reports or other security analysis tools.
  • Input Validation: Although the service consumes from Pub/Sub (an internal system), it is good practice to validate received JSON payload data (e.g. email format, attachment size if relevant) to avoid unexpected errors or abuse. Spring Validation (spring-boot-starter-validation) is included and can be used in DTOs.
  • Logging: Ensure sensitive information such as credentials or full attachment content is not logged, unless strictly necessary for debugging and with the appropriate log level.