Milesoft Twilio Extension (twilio-ext)
1. Overview & Purpose
The Twilio Extension (twilio-ext) is a secure, unified, and lightweight JVM facade that encapsulates both the official Twilio Java SDK and the SendGrid Java SDK. It ensures consistent, high-delivery messaging, transactional SMS routing, and multi-factor authentication (MFA)/phone verification workflows across all Milesoft enterprise applications.
By abstracting away raw SDK boilerplate and handling structural details under-the-hood, twilio-ext enables seamless and secure digital communication.
Key Benefits & Core Capabilities:
- Unified Communications Interface: Streamlines interactions with SendGrid (for emails) and Twilio (for SMS/Verify), eliminating low-level dependency management in downstream services.
- Phone Verification Workflows (
VerifyService): Integrates directly with Twilio's native Verify v2 API to dispatch and validate security codes via SMS or voice calls. - Precision SMS Alerts (
SmsService): Provides a robust system to format, serialize, and deliver transactional outbound text messages. - Pre-Templated HTML Email Wrappers (
EmailService): Provides automatic templating utilizingEmailTemplateUtilsto embed plain-text content inside beautiful, responsive HTML email structures. - Google
libphonenumberIntegration: Features built-in phone number sanitization (PhoneUtils) that formats raw user numbers into strict E.164 formats, preventing formatting-related API errors.
2. Installation & Dependency Configuration
The Twilio Extension library is hosted inside our secure Google Cloud Artifact Registry repository.
To configure your Gradle application to resolve and pull this extension, configure the repositories and dependency blocks inside your build.gradle:
2.1 Gradle Setup (build.gradle)
plugins {
id "com.google.cloud.artifactregistry.gradle-plugin" version "2.2.5"
id "java"
}
repositories {
mavenLocal()
mavenCentral()
maven {
url "artifactregistry://us-west2-maven.pkg.dev/milesoft-repo/repo-maven"
}
}
dependencies {
// Required base platform utilities
implementation "io.milesoft:commons:2.0.1"
// Milesoft Twilio & SendGrid Integration Extension
implementation "io.milesoft:twilio-ext:2.0.1"
}
3. Configuring Twilio & SendGrid in Spring Boot
To leverage dependency injection within your application, register the core communication classes as Spring-managed beans, resolving sensitive credentials from Google Cloud Secret Manager at startup.
3.1 Properties Configuration
In your properties file (e.g., application.yml or application.properties), define the target secret keys stored in GCP Secret Manager along with sender configurations:
secrets:
twilio:
# Keys identifying Twilio credentials within GCP Secret Manager
accountSid: "prod-twilio-account-sid"
accountToken: "prod-twilio-auth-token"
serviceSid: "prod-twilio-verify-service-sid"
sid: "prod-twilio-sms-sid"
token: "prod-twilio-sms-token"
sendGrid:
# Key identifying the SendGrid API key within GCP Secret Manager
apiKey: "prod-sendgrid-api-key"
twilio:
from: "+15551234567" # Your active outbound Twilio number
sendGrid:
from: "noreply@milesoft.io"
3.2 Twilio Configuration Class
Create a @Configuration class to resolve keys via SecretManagerService and provision the singleton instances of EmailService, SmsService, and VerifyService:
import static com.google.common.base.Preconditions.checkNotNull;
import io.milesoft.secrets.services.SecretManagerService;
import io.milesoft.twilio.services.EmailService;
import io.milesoft.twilio.services.SmsService;
import io.milesoft.twilio.services.VerifyService;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class TwilioConfig {
@Bean
public EmailService emailService(
SecretManagerService secretManagerService,
@Value("${secrets.sendGrid.apiKey}") String secretApiKey,
@Value("${sendGrid.from}") String emailFrom) {
checkNotNull(secretManagerService, "secretManagerService must not be null");
final String apiKey = secretManagerService.loadSecret(secretApiKey);
return new EmailService(apiKey, emailFrom);
}
@Bean
public SmsService smsService(
SecretManagerService secretManagerService,
@Value("${secrets.twilio.sid}") String secretSid,
@Value("${secrets.twilio.token}") String secretToken,
@Value("${twilio.from}") String smsFrom) {
checkNotNull(secretManagerService, "secretManagerService must not be null");
final String sid = secretManagerService.loadSecret(secretSid);
final String token = secretManagerService.loadSecret(secretToken);
return new SmsService(sid, token, smsFrom);
}
@Bean
public VerifyService verifyService(
SecretManagerService secretManagerService,
@Value("${secrets.twilio.accountSid}") String secretAccountSid,
@Value("${secrets.twilio.accountToken}") String secretAccountToken,
@Value("${secrets.twilio.serviceSid}") String secretServiceSid) {
checkNotNull(secretManagerService, "secretManagerService must not be null");
final String accountSid = secretManagerService.loadSecret(secretAccountSid);
final String accountToken = secretManagerService.loadSecret(secretAccountToken);
final String serviceSid = secretManagerService.loadSecret(secretServiceSid);
return new VerifyService(accountSid, accountToken, serviceSid);
}
}
4. Domain & Data Types
The library enforces strict type safety and structural integrity through lightweight, builder-based domain classes.
4.1 Email Domain Model (Email)
Encapsulates transaction-driven email metadata, supporting optional names, reply-to fields, and structured content representation.
import lombok.Builder;
import lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.NonNull;
import lombok.ToString;
@Getter
@EqualsAndHashCode
@ToString
@Builder(builderClassName = "Builder")
public class Email {
private final String replyTo; // Optional custom reply-to email address
private final String fromName; // Optional friendly sender display name (e.g., "Milesoft Platform")
@NonNull
private final String to; // Recipient email address
@NonNull
private final String subject; // Email header subject line
private final String textBody; // Fallback raw text representation
private final String htmlBody; // Rich HTML content payload
}
4.2 SMS Domain Model (Sms)
An immutable representation of an outbound text payload. The sender's originating line is managed securely at the service level.
import lombok.Builder;
import lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.NonNull;
import lombok.ToString;
@Getter
@EqualsAndHashCode
@ToString
@Builder(builderClassName = "Builder")
public class Sms {
@NonNull
private final String to; // Target phone number (sanitized to E.164 under-the-hood)
@NonNull
private final String body; // Message text content
}
4.3 Verification Channel (Method)
Enforces strict typing on the channel used to dispatch security codes.
public enum Method {
Sms, // Sends code via Text message
Call // Reads code via Voice call
}
5. API Reference Guide
5.1 SmsService
Manages outbound SMS routing and message processing via standard Twilio long codes.
Constructor:
public SmsService(String sid, String token, String from)
- Configures the internal
TwilioRestClientusing the provided Twilio Account SID and API Token, and sets the default outbound originating phone line.
Methods:
| Method Signature | Return Type | Description |
|---|---|---|
sendSms(Sms sms) |
void |
Delivers an SMS payload. If Twilio returns a failed state (FAILED, CANCELED, UNDELIVERED), throws a detailed IllegalStateException for handling upstream. |
5.2 VerifyService
Orchestrates secure multi-factor phone number verification without managing custom code databases.
Constructor:
public VerifyService(String accountSid, String accountToken, String serviceSid)
- Configures the Verify service client under a specific, dedicated Twilio Verification Service SID container.
Methods:
| Method Signature | Return Type | Description |
|---|---|---|
sendCode(String phone, Method method) |
String |
Formats the input number to E.164 and triggers an outbound code dispatch via the specified Method. Returns the unique verification sequence SID. |
isValid(String phone, String code) |
boolean |
Verifies a user-supplied OTP code. Safely intercepts exceptions (like expired/invalid code mismatches or 404s) and returns false gracefully. |
5.3 EmailService
A high-throughput service layer orchestrating transactional email delivery via the SendGrid API.
Constructor:
public EmailService(String apiKey, String from)
- Instantiates a SendGrid-based client using the secure authorization API key and binds a default outbound sending address.
Methods:
| Method Signature | Return Type | Description |
|---|---|---|
sendEmail(Email email) |
void |
Dispatches an email transaction. Expects at least one of textBody or htmlBody to be populated. On delivery failures (non-2xx response), throws an IllegalStateException with response details. |
5.4 Utility Classes
PhoneUtils
An internal international helper utility integrating Google's metadata project:
formatE164(String phone): Dynamic sanitization parsing. For example:555-555-5555$\rightarrow$+15555555555.
EmailTemplateUtils
Standardizes the visual format of our transactional layouts:
format(String content): Resolves and reads our core layout/html/email_template.htmlfrom resources, replaces line-break chars (\n$\rightarrow$<br>), and injects the dynamic string in place of the~CONTENT~token.
6. Practical Real-World Code Examples
These production-inspired patterns demonstrate how to securely integrate twilio-ext within your microservice architectures.
6.1 MFA and Login Flow (Using VerifyService)
Shows how standard user authentication layers handle two-factor text confirmation sequences seamlessly.
import static com.google.common.base.Preconditions.checkNotNull;
import io.milesoft.accounts.domain.User;
import io.milesoft.twilio.enums.Method;
import io.milesoft.twilio.services.VerifyService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
@Slf4j
@Service
public class LoginService {
private final VerifyService verifyService;
public LoginService(VerifyService verifyService) {
this.verifyService = checkNotNull(verifyService, "verifyService must not be null");
}
/**
* Step 1: Issue OTP code during high-security operations (e.g., login, password recovery)
*/
public void initiateTwoFactorAuth(User user) {
final String phone = user.getPhone();
log.info("Initiating 2FA check for user {}. Sending SMS OTP...", user.getId());
try {
// SendCode formats phone number to E.164 automatically
String verificationSid = verifyService.sendCode(phone, Method.Sms);
log.info("2FA code dispatched. Verification SID: {}", verificationSid);
} catch (Exception e) {
log.error("Failed to dispatch Twilio verification code to {}", phone, e);
throw new IllegalStateException("Authentication service temporarily unavailable");
}
}
/**
* Step 2: Validate code received from user input
*/
public boolean confirmTwoFactorAuth(String phone, String userInputCode) {
log.info("Checking OTP code validity for phone {}...", phone);
// isValid returns false gracefully on expired or invalid inputs
boolean isAuthorized = verifyService.isValid(phone, userInputCode);
if (isAuthorized) {
log.info("MFA success. Granting access tokens.");
} else {
log.warn("MFA failed. Input OTP {} is invalid or has expired.", userInputCode);
}
return isAuthorized;
}
}
6.2 Transactional Alerting (Using SmsService & EmailService)
Shows a multi-channel dispatcher sending custom formatted communications during account setup or workspace actions.
import static com.google.common.base.Preconditions.checkNotNull;
import io.milesoft.twilio.domain.Email;
import io.milesoft.twilio.domain.Sms;
import io.milesoft.twilio.services.EmailService;
import io.milesoft.twilio.services.SmsService;
import io.milesoft.twilio.utils.EmailTemplateUtils;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
@Slf4j
@Service
public class MultiChannelNotificationDispatcher {
private final EmailService emailService;
private final SmsService smsService;
public MultiChannelNotificationDispatcher(EmailService emailService, SmsService smsService) {
this.emailService = checkNotNull(emailService, "emailService must not be null");
this.smsService = checkNotNull(smsService, "smsService must not be null");
}
public void dispatchWorkspaceActivation(String emailAddress, String phoneNumber, String adminName) {
log.info("Dispatching multi-channel activation alert for Admin: {}", adminName);
// 1. Compose and send a beautiful HTML welcome email utilizing our template utilities
String messageBody = String.format(
"Hello %s,\n\nYour Workspace is now fully active.\nExplore your dashboard to start managing tasks.",
adminName
);
Email email = Email.builder()
.fromName("Your App")
.to(emailAddress)
.subject("Your Workspace is Ready!")
// Automatically wrap text in standard branding HTML
.htmlBody(EmailTemplateUtils.format(messageBody))
.build();
try {
emailService.sendEmail(email);
log.info("Welcome email sent to {}", emailAddress);
} catch (Exception e) {
log.error("Failed to send welcome email to {}", emailAddress, e);
}
// 2. Send SMS to notify them on their mobile device
Sms sms = Sms.builder()
.to(phoneNumber)
.body("Your App Alert: Your workspace is ready! Check your inbox for login instructions.")
.build();
try {
smsService.sendSms(sms);
log.info("Notification SMS sent to {}", phoneNumber);
} catch (Exception e) {
log.error("Failed to send activation SMS to {}", phoneNumber, e);
}
}
}
7. Troubleshooting & FAQs
Q: Why does VerifyService catch general exceptions in isValid?
A: When checking an OTP, the Twilio Verify API returns a 404 Not Found response if a verification code has expired, already been successfully checked, or was never initially requested. VerifyService.isValid safely intercepts these exceptions, logs a warning via SLF4J, and returns false to keep application execution flows continuous without causing crashes.
Q: Does EmailTemplateUtils.format handle formatting of line breaks?
A: Yes! Under-the-hood, the utility parses line-break characters (\n) using HtmlUtils.newToBr(content) to ensure that visual structures are fully preserved as HTML break tags (<br>) inside the wrapper layout.
Q: How does PhoneUtils handle parsing international numbers?
A: It delegates to Google's standard libphonenumber library, parsing relative to a default Locale.US context unless a country code is explicitly included (e.g. +1 or +44). Passing completely invalid numbers triggers exceptions, so sanitizing inputs upstream is recommended.