EmailServiceConfig entry in emailServiceConfig.jrl with SMTP credentialsEmailTemplate entries in emailTemplates.jrl with your email body/subjectEmailConfig entry in emailConfig.jrl for per-SPID defaultsEmailMessage with status: DRAFT to emailMessageDAO - the system handles the rest+-------------------+ +-------------------+ +-------------------+
| EmailServiceConfig| | EmailTemplate | | EmailConfig |
+-------------------+ +-------------------+ +-------------------+
| HOW to send | | WHAT to send | | WHO sends it |
| | | | | |
| - SMTP host/port | | - Subject | | - from address |
| - Credentials | | - Body HTML | | - replyTo address |
| - TLS settings | | - Variables | | - displayName |
| - Poll interval | | - Per group/locale| | - Per SPID |
+-------------------+ +-------------------+ +-------------------+
EmailServiceConfig - “How to connect to the mail server”
The connection configuration that tells the system HOW to send emails: SMTP host, port, username/password, TLS settings. Think of it as the mail server login credentials.
EmailTemplate - “What the email looks like”
The email content - the actual subject and body with variable placeholders like ``. Templates can vary by group, locale, and SPID. Think of it as a reusable email blueprint.
EmailConfig - “Who sends the email”
The sender defaults per SPID (tenant) - default from, replyTo, and displayName values. Think of it as the “From:” line defaults for each tenant.
The FOAM3 Email System is a comprehensive, rule-driven email processing pipeline that:
| File | Purpose |
|---|---|
EmailMessage.js |
Core email data model with status tracking |
EmailServiceConfig.js |
SMTP/IMAP connection configuration |
EmailTemplate.js |
Reusable email templates with variable support |
EmailConfig.js |
Per-SPID default sender configuration |
SMTPAgent.js |
SMTP sending agent (polls for UNSENT emails) |
EmailFolderAgent.js |
IMAP receiving agent (polls for new emails) |
MicrosoftGraphEmailAgent.js |
Microsoft 365 sending via Graph API |
ChainedPropertyService.js |
Orchestrates the property service pipeline |
EmailTemplateEngine.js |
Template variable substitution engine |
EmailTemplateSupport.java |
Template lookup with fallback logic |
+-----------------------------------------------------------------------------------+
| FOAM3 Email System |
+-----------------------------------------------------------------------------------+
| |
| +-------------------------+ +-------------------------+ |
| | EMAIL CREATION | | EMAIL RECEIVING | |
| | (Notification/Direct) | | (IMAP/POP3) | |
| +------------+------------+ +------------+------------+ |
| | | |
| v v |
| +------------+------------+ +------------+------------+ |
| | emailMessageDAO | | emailMessageReceivedDAO | |
| | (status: DRAFT) | | (status: RECEIVED) | |
| +------------+------------+ +------------+------------+ |
| | | |
| v v |
| +------------+---------------------------------------------------+ |
| | RULE ENGINE | |
| | +------------------+ +------------------+ +----------------+ | |
| | | PropertyService | | Status Rule | | Processed Rule | | |
| | | Rule (pri:1000) | | (pri:100) | | (pri:0) | | |
| | +------------------+ +------------------+ +----------------+ | |
| +----------------------------------------------------------------+ |
| | |
| v |
| +------------+------------+ |
| | emailMessageDAO | |
| | (status: UNSENT) | |
| +------------+------------+ |
| | |
| v |
| +------------+-------------------------------------------+ |
| | EMAIL AGENTS | |
| | +----------------+ +----------------+ +------------+ | |
| | | SMTPAgent | | MSGraphAgent | | FolderAgent| | |
| | | (id: smtp) | | (id: msGraph) | | (id: imap) | | |
| | +----------------+ +----------------+ +------------+ | |
| +--------------------------------------------------------+ |
| | |
| v |
| +------------+------------+ |
| | EXTERNAL SERVERS | |
| | SMTP / Graph API | |
| +-------------------------+ |
| |
+-----------------------------------------------------------------------------------+
+-----------------------------------------------------------------------------------+
| ChainedPropertyService |
+-----------------------------------------------------------------------------------+
| |
| EmailMessage (DRAFT) |
| | |
| v |
| +----+------------------------------------------+ |
| | 1. ApplyBaseArgumentsEmailPropertyService | |
| | - Sets SPID from user | |
| | - Loads theme, appConfig, supportConfig | |
| | - Populates templateArgs (logo, appLink, | |
| | supportEmail, hostname, etc.) | |
| +-----------------------------------------------+ |
| | |
| v |
| +----+------------------------------------------+ |
| | 2. EmailTemplateApplyEmailPropertyService | |
| | - Finds EmailTemplate by name/group/locale | |
| | - Applies template to emailMessage | |
| | - Renders body, subject with variables | |
| +-----------------------------------------------+ |
| | |
| v |
| +----+------------------------------------------+ |
| | 3. GroupEmailTemplateService | |
| | - Walks group hierarchy for replyTo, from | |
| | - Inherits from parent groups | |
| +-----------------------------------------------+ |
| | |
| v |
| +----+------------------------------------------+ |
| | 4. EmailConfigEmailPropertyService | |
| | - Loads EmailConfig by SPID | |
| | - Sets replyTo, displayName, from, to | |
| +-----------------------------------------------+ |
| | |
| v |
| +----+------------------------------------------+ |
| | 5. EmailMessageValidationPropertyService | |
| | - Validates: to, subject, body are set | |
| | - Throws InvalidParameterException if not | |
| +-----------------------------------------------+ |
| | |
| v |
| EmailMessage (validated, ready for UNSENT) |
| |
+-----------------------------------------------------------------------------------+
| Status | Ordinal | Description |
|---|---|---|
| DRAFT | 0 | Initial state - triggers rule processing |
| UNSENT | 1 | Processed and validated - ready for agent pickup |
| SENT | 2 | Successfully delivered to mail server |
| FAILED | 3 | Delivery failed (permanent) |
| BOUNCED | 4 | Message bounced back |
| RECEIVED | 5 | Inbound email received via IMAP/POP3 |
| PROCESSED | 6 | Received email has been processed |
Configuration values are resolved in order of precedence (first match wins):
The EmailTemplateEngine supports three syntax patterns:
| Syntax | Purpose | Example |
|---|---|---|
| Simple variable substitution | |
||
| Object property access | |
||
| Conditional blocks | |
||
... |
Conditional with else | ... |
{$ tokenName $} |
CSS token substitution | {$ primary500 $} |
<include template='name'>...</include> |
Template inheritance | See Template Inheritance section |
Base configuration for email service connections (SMTP/IMAP).
| Property | Type | Default | Description |
|---|---|---|---|
| id | String | ‘default’ | Unique identifier (e.g., ‘smtp’, ‘imap’, ‘msGraph’) |
| enabled | Boolean | false | Whether this config is active |
| host | String | ‘127.0.0.1’ | Mail server hostname |
| port | String | ‘587’ | Mail server port |
| username | String | null | Authentication username |
| password | Password | null | Authentication password |
| authenticate | Boolean | true | Whether to use authentication |
| starttls | Boolean | true | Whether to use STARTTLS |
| protocol | String | ‘smtp’ | Protocol: ‘smtp’ or ‘imaps’ |
| pollInterval | Long | 10000 | Agent polling interval (ms) |
| initialDelay | Int | 60000 | Agent startup delay (ms) |
| folderName | String | ‘INBOX’ | Folder to fetch (IMAP only) |
| delete | Boolean | false | Delete after fetch (IMAP only) |
| processAttachments | Boolean | true | Process attachments (IMAP only) |
Additional properties for Microsoft Graph API authentication.
| Property | Type | Description |
|---|---|---|
| clientId | String | Microsoft Graph API client ID |
| tenantId | String | Microsoft Graph API tenant ID |
| password | String | Client secret (uses base class password field) |
Per-SPID email defaults (keyed by SPID).
| Property | Type | Description |
|---|---|---|
| spid | Reference | Service Provider ID (primary key) |
| from | String | Default sender email address |
| displayName | String | Default sender display name |
| replyTo | String | Default reply-to address |
Reusable email templates with variable support.
| Property | Type | Description |
|---|---|---|
| id | String | Unique identifier |
| name | String | Template name (used for lookup) |
| group | String | Group filter (‘*’ for all) |
| locale | String | Locale filter (‘en’ default) |
| spid | String | SPID filter (empty for all) |
| subject | String | Email subject (supports variables) |
| body | String | Email body HTML (supports variables) |
| displayName | String | Sender display name |
| sendTo | String | Default recipient |
| replyTo | String | Reply-to address |
| enabled | Boolean | Whether template is active |
| sourceClass | String | Tracking: which class uses this template |
The core email data model.
| Property | Type | Description |
|---|---|---|
| id | String | Unique message identifier |
| user | Reference | Target user for this email |
| status | Enum | Current status (DRAFT, UNSENT, SENT, etc.) |
| from | String | Sender address |
| displayName | String | Sender display name |
| replyTo | String | Reply-to address |
| to | StringArray | Recipient addresses |
| cc | StringArray | CC addresses |
| bcc | StringArray | BCC addresses |
| subject | String | Email subject |
| body | String | Email body (HTML) |
| templateArguments | Map | Variables for template rendering |
| attachments | StringArray | File IDs for attachments |
| sentDate | DateTime | When the email was sent |
| created | DateTime | When the message was created |
| spid | Reference | Service provider ID |
The primary sending agent for SMTP protocol.
Service ID: smtp (references EmailServiceConfig with id=’smtp’)
Behavior:
start() using config’s initialDelay and pollIntervalemailMessageDAO for messages with status=UNSENTsmtpAgentThrottle), then sendsKey Methods:
start() - Initialize timer and begin pollingstop() - Cancel timerexecute() - Process unsent emailssend() - Send individual email via SMTPcreateMimeMessage() - Convert EmailMessage to MimeMessagereload() - Hot-reload configuration changesThe receiving agent for IMAP/POP3 protocols.
Service ID: imap (references EmailServiceConfig with id=’imap’)
Behavior:
start() using config’s initialDelay and pollIntervalemailMessageReceivedDAOSending agent for Microsoft 365 via Graph API (extends SMTPAgent).
Service ID: msGraph (references ms.EmailServiceConfig with id=’msGraph’)
Behavior:
https://graph.microsoft.com/.defaultPOST https://graph.microsoft.com/v1.0/users/<username>/sendMail) with the OAuth bearer tokenmicrosoftGraphEmailAgentThrottle (default: 4 TPS)Configuration Requirements:
clientId - Azure AD application client IDtenantId - Azure AD tenant IDpassword - Client secretusername - Email address (mailbox) to send fromThe emailPropertyService is a ChainedPropertyService that processes each EmailMessage through a pipeline of services. Each service in the chain can modify the email, adding properties that weren’t already set.
| Order | Service | Priority | Responsibility |
|---|---|---|---|
| 1 | ApplyBaseArgumentsEmailPropertyService | Highest | Sets SPID, loads theme/support config, populates base templateArgs |
| 2 | EmailTemplateApplyEmailPropertyService | - | Finds template, renders body/subject/displayName/replyTo/sendTo |
| 3 | GroupEmailTemplateService | - | Inherits replyTo/displayName/from from group hierarchy |
| 4 | EmailConfigEmailPropertyService | - | Applies per-SPID defaults from EmailConfig |
| 5 | EmailMessageValidationPropertyService | Lowest | Validates required fields (to, subject, body) |
The ApplyBaseArgumentsEmailPropertyService automatically populates these variables:
| Variable | Source | Description |
|---|---|---|
| logo | Theme | URL to theme logo |
| largeLogo | Theme | URL to large theme logo |
| appLink | AppConfig | Application base URL |
| appName | Theme | Application name |
| locale | User | User’s language code |
| supportAddress | SupportConfig | Support postal address |
| supportPhone | SupportConfig | Support phone number |
| supportEmail | SupportConfig | Support email address |
| supportLogo | SupportConfig | Support logo URL |
| termsAndCondLink | AppConfig | Terms and conditions URL |
| termsAndCondLabel | AppConfig | Terms and conditions label |
| copyright | AppConfig | Copyright text |
| privacyUrl | AppConfig | Privacy policy URL |
| privacyLabel | AppConfig | Privacy policy label |
| personalSupportPhone | SupportConfig | Personal support user phone |
| personalSupportEmail | SupportConfig | Personal support user email |
| personalSupportFirstName | SupportConfig | Personal support user first name |
| personalSupportFullName | SupportConfig | Personal support user full name |
| hostname | System | Server hostname |
| theme | Theme | Full theme object for advanced use |
The EmailTemplateSupport.findTemplate() method searches for templates with cascading fallback:
Search Order (first match wins):
+-------+-------+--------+------+
| Name | Group | Locale | SPID |
+-------+-------+--------+------+
| Y | G* | Y | Y | <- Most specific
| Y | G* | Y | "" |
| Y | G* | en | Y |
| Y | G* | en | "" |
| Y | * | en | Y |
| Y | * | en | "" | <- Least specific (catch-all)
+-------+-------+--------+------+
G* means the user’s group, then each ancestor group in turn: every row walks the whole
group hierarchy before the next row is tried. An empty SPID ("") matches templates with no
SPID set.
Templates can extend other templates using the <include> tag:
Parent Template (base-email):
+----------------------------------+
| <html> |
| <head>...</head> |
| <body> |
| <header>Logo</header> |
| <content></content> | <- Insertion point
| <footer>Copyright</footer> |
| </body> |
| </html> |
+----------------------------------+
Child Template (welcome-email):
+----------------------------------+
| <include template='base-email'> |
| <h1>Welcome!</h1> |
| <p>Hello </p> |
| </include> |
+----------------------------------+
Rendered Result:
+----------------------------------+
| <html> |
| <head>...</head> |
| <body> |
| <header>Logo</header> |
| <h1>Welcome!</h1> |
| <p>Hello John</p> |
| <footer>Copyright</footer> |
| </body> |
| </html> |
+----------------------------------+
Three rules process emails, on localEmailMessageDAO and localEmailMessageReceivedDAO:
| Rule ID | Priority | Predicate | Action | Purpose |
|---|---|---|---|---|
| foam-core-notification-email-EmailMessagePropertyServiceRule | 1000 | status=DRAFT | EmailMessagePropertyServiceRuleAction | Process through property service chain |
| foam-core-notification-email-EmailMessageStatusRule | 100 | status=DRAFT | EmailMessageStatusRuleAction | Set status to UNSENT |
| foam-core-notification-email-EmailMessageProcessedStatusRule | 0 | status=PROCESSED | DAOPutRuleAction | Move to emailMessageProcessedDAO |
EmailMessage PUT (status: DRAFT)
|
v
+-------+-------+
| Rule Engine |
| daoKey: |
| localEmail- |
| MessageDAO |
+-------+-------+
|
| Priority 1000
v
+-------+-------+
| PropertySvc |
| Rule |
| - Apply chain |
| - Render |
| template |
| - Validate |
+-------+-------+
|
| Priority 100
v
+-------+-------+
| Status Rule |
| - Set status |
| to UNSENT |
+-------+-------+
|
v
EmailMessage (status: UNSENT)
|
v
SMTPAgent picks up and sends
| DAO | Purpose | Journal |
|---|---|---|
| localEmailTemplateDAO | Template storage | emailTemplates |
| emailTemplateDAO | Template access (wraps local) | - |
| localEmailMessageDAO | Outbound message storage | emailMessages |
| emailMessageDAO | Outbound message access | - |
| localEmailMessageReceivedDAO | Inbound message storage | emailMessagesReceived |
| emailMessageReceivedDAO | Inbound message access | - |
| localEmailMessageProcessedDAO | Processed inbound storage | emailMessagesProcessed |
| emailMessageProcessedDAO | Processed inbound access | - |
| emailConfigDAO | Per-SPID config | emailConfig |
| emailServiceConfigDAO | Connection config | emailServiceConfig |
OUTBOUND EMAIL FLOW:
+-------------------+ +-------------------+ +-------------------+
| Application Code | | localEmailMessage | | Rule Engine |
| creates |---->| DAO |---->| processes |
| EmailMessage | | (status: DRAFT) | | (status: UNSENT) |
| (status: DRAFT) | | | | |
+-------------------+ +-------------------+ +--------+----------+
|
+-------------------+ |
| SMTPAgent or |<-------------+
| MSGraphAgent |
| polls & sends |
| (status: SENT/ |
| FAILED) |
+-------------------+
INBOUND EMAIL FLOW:
+-------------------+ +-------------------+ +-------------------+
| EmailFolderAgent | | emailMessage- | | Application |
| polls IMAP |---->| ReceivedDAO |---->| processes |
| folder | | (status: RECEIVED)| | (status: PROCESSED|
+-------------------+ +-------------------+ +--------+----------+
|
+-------------------+ |
| emailMessage- |<-------------+
| ProcessedDAO |
| (archival) |
+-------------------+
To use Microsoft Graph for sending emails:
Mail.Send permission (application permission)class: foam.core.notification.email.ms.EmailServiceConfigid: 'msGraph' (not ‘msGraphConfig’ or other names)clientId, tenantId, password (client secret), username (sending address)microsoftGraphEmailAgent service references config with id: 'msGraph'enabled: true in services.jrl to activate+----------------------------------+ +----------------------------------+
| MicrosoftGraphEmailAgent | | ms.EmailServiceConfig |
| | | |
| properties: | | Journal Entry: |
| id: { | | id: 'msGraph' <-- MUST MATCH |
| class: 'Reference', |---->| clientId: '...' |
| value: 'msGraph' <-- | | tenantId: '...' |
| } | | password: '...' |
+----------------------------------+ +----------------------------------+
Issue: Microsoft Graph emails not sending; agent can’t find config.
Solution: The MicrosoftGraphEmailAgent looks for config with id: 'msGraph' (not ‘msGraphConfig’). Ensure your config entry uses id: 'msGraph' exactly.
Issue: Email body is empty or uses wrong template.
Solution: Check template lookup order:
name or id*enIssue: Email stuck in DRAFT status.
Solution: Ensure the email rules are enabled in rules.jrl. The rules trigger on PUT to localEmailMessageDAO when status=DRAFT.
Issue: Emails stuck in UNSENT status.
Solution:
EmailServiceConfig.enabled is truelazy: false - they start at bootIssue: Email shows `` instead of value.
Solution:
templateArguments mapIssue: Attachments missing from sent email.
Solution:
fileDAOIdentifiedBlob and InputStreamBlob are supportedIssue: Emails failing with “Too many login attempts” or similar.
Solution:
foam.core.pool.Throttle.rate if neededIssue: SVG images not displaying in email clients.
Solution: The EmailMessagePropertyServiceRuleAction automatically replaces .svg extensions with .png in the body. Ensure PNG versions exist at same paths.
Issue: Emails reprocessed after restart.
Solution: Set EmailServiceConfig.delete: true to delete after processing. Default (false) only marks as SEEN, which may not persist across restarts with some providers.
foam3/doc/guides/email-setup.md - Step-by-step setup guidefoam3/src/foam/core/notification/ - Notification system (often triggers emails)foam3/src/foam/core/theme/ - Theme system (provides email styling context)