Core Logic
Config resolution
Email sending uses the following default lookup order:
1. Current tenant default mail server
2. Platform default mail server (`tenant_id = 0`)
3. BusinessException if nothing is availableIf multiple records are marked as default, the one with the smallest sortOrder is used.
Template resolution
Email templates use the following fallback logic:
tenant + current language
-> tenant + default language
-> platform + current language
-> platform + default language
-> BusinessExceptionTemplate placeholders use the unified Softa syntax: {{ variable }}.
Async delivery
sendAsync(...)returns immediately and performs delivery in the background- Every send automatically creates a
MailSendRecord
Business code usually does not need to choose a mail server explicitly. Defaults should be prepared by the platform or tenant admin.
Sending Email
Inject MailSendService where email delivery is needed:
@Autowired
private MailSendService mailSendService;
// Plain text
mailSendService.sendText("[email protected]", "Hello", "Welcome to Softa.");
// HTML
mailSendService.sendHtml("[email protected]", "Welcome", "<h1>Hello Alice</h1>");
// Multiple recipients
mailSendService.sendHtml(List.of("[email protected]", "[email protected]"), "Notice", "<p>...</p>");
// Full control
SendMailDTO dto = new SendMailDTO();
dto.setTo(List.of("[email protected]"));
dto.setCc(List.of("[email protected]"));
dto.setSubject("Offer Letter");
dto.setHtmlBody("<p>Dear Alice...</p>");
dto.setAttachments(List.of(attachment));
mailSendService.send(dto);
// Async
mailSendService.sendAsync(dto);Attachments
MailAttachmentDTO attachment = new MailAttachmentDTO();
attachment.setFileName("report.pdf");
attachment.setContentType("application/pdf");
attachment.setData(pdfBytes);
// or
attachment.setFileId(fileId);Email Templates
Use templates when business content should be reusable or multilingual:
@Autowired
private MailSendService mailSendService;
Map<String, Object> vars = Map.of(
"name", "Alice",
"activationUrl", "https://app.example.com/activate/abc123"
);
mailSendService.sendByTemplate("USER_WELCOME", "[email protected]", vars);
mailSendService.sendByTemplate("ORDER_CONFIRMATION", List.of("[email protected]", "[email protected]"), vars);The current request language is taken from ContextHolder.
Template example
POST /MailTemplate/createOne
{
"code": "USER_WELCOME",
"name": "User Welcome Email",
"language": "en-US",
"subject": "Welcome, {{ name }}!",
"body": "<h1>Welcome, {{ name }}</h1><p><a href='{{ activationUrl }}'>Activate</a></p>",
"includePlainText": true,
"isEnabled": true
}Use language: "default" to define a fallback template.
Receiving Email
If the business needs inbound mail processing, inject MailReceiveService:
@Autowired
private MailReceiveService mailReceiveService;
// Fetch from auto-resolved server
int fetched = mailReceiveService.fetchNewMails();
// Fetch from a specific server config
int fetchedByServer = mailReceiveService.fetchNewMails(serverConfigId);
// Mark as read
mailReceiveService.markAsRead(recordId);
mailReceiveService.markAsRead(List.of(id1, id2, id3));Messages are deduplicated by (server_config_id, message_id), so repeated polling is safe.
Email Status Reference
MailSendRecord
PENDING -> SENT
PENDING -> FAILED
PENDING -> RETRYMailReceiveRecord
UNREAD -> READ -> ARCHIVED
-> DELETED