A Frappe v16 app that handles contact-form enquiries from multiple client websites. Each website gets its own Website Master record that controls email routing, captcha validation, CORS, and email templates, all managed from the Frappe desk.
- Multi-site: one Frappe instance serves any number of client websites, each with independent configuration
- Async email: enquiries are queued via Frappe Email Queue; no SMTP latency on the API response
- Delivery tracking: Website Enquiry status advances automatically (New -> Email Queued -> Sent / Email Failed) as the Email Queue processes
- Retry scheduler: failed emails are retried every 15 minutes up to the configured Max Retry Count
- Dynamic sender: sender name and address are read from the linked Frappe Email Account record, not hardcoded
- Multi-recipient: admin notifications can go to multiple addresses (comma-separated); each Website Master also supports CC, BCC, a Jinja custom subject, and a default file attachment
- Email templates: default Mailbird templates ship as fixtures; any Website Master can override with a custom Frappe Email Template using Jinja variables; all outgoing emails carry a "Powered by Techbird" footer
- Captcha: Cloudflare Turnstile and Google reCAPTCHA v2/v3 supported; type chosen per Website Master
- CORS sync:
allowed_domainson a Website Master are written tosite_config.jsonautomatically on save; removals and deactivation are also reflected - Domain validation: wildcards, non-HTTP schemes, paths, and out-of-range ports are rejected before save
- Webhook delivery: form submissions can be POSTed as JSON to any URL (Slack, Zapier, CRM, etc.)
- Honeypot: hidden field
website_urlsilently drops bot submissions - App settings: retry count, message length, webhook timeout, and data retention are configurable from the desk via Mailbird Settings (Single DocType)
- Error logging: Mailbird-specific errors are captured automatically into a dedicated Mailbird Error Log DocType for triage without searching the global error log
cd $PATH_TO_YOUR_BENCH
bench get-app $URL_OF_THIS_REPO --branch version-16
bench install-app mailbird
bench --site <your-site> migrateOne record per client website. Fields:
| Field | Description |
|---|---|
| Website Name | Display name, used as the autoname key |
| Domain Name | Bare domain (e.g. example.in), matched against the site form field |
| Client Name | Internal label |
| Recipient Email(s) | Comma-separated email addresses that receive admin notifications |
| CC Email(s) | Comma-separated addresses to CC on every admin notification |
| BCC Email(s) | Comma-separated addresses to BCC on every admin notification |
| Custom Email Subject | Jinja template for the admin email subject (variables: visitor_name, visitor_email, subject, website_name, site). Leave blank to use the default |
| Default Attachment | File attached to every admin notification email (e.g. a brochure or rate card) |
| Email Account | Frappe Email Account used as the sender; name and address are read dynamically |
| Admin Notification Template | Frappe Email Template for the admin email. Leave blank to use the default Mailbird template |
| Auto Reply Enabled | Toggle visitor auto-reply on/off |
| Auto Reply Template | Frappe Email Template for the visitor receipt. Leave blank to use the default Mailbird template |
| Active | Inactive sites are ignored by the API and removed from allow_cors |
| Configuration Status | Manual status indicator (Pending / Configured / Validated / Failed) |
| Allowed Domains | One origin per line (e.g. https://example.in). Written to site_config.json on save. Plain hostnames have https:// prepended automatically |
| Captcha Type | Turnstile (default) or reCAPTCHA |
| Turnstile Site Key / Secret Key | Cloudflare Turnstile credentials (shown when Captcha Type is Turnstile) |
| reCAPTCHA Site Key / Secret Key | Google reCAPTCHA credentials (shown when Captcha Type is reCAPTCHA) |
| Webhook URL | POST destination for each new enquiry payload |
| Notes | Free-text internal notes |
Audit record created for each form submission. Fields: visitor_name, visitor_email, visitor_phone, enquiry_subject, message, ip_address, submitted_at, status, retry_count, webhook_delivered, honeypot_triggered.
Status flow:
New -> Email Queued -> Sent
-> Email Failed (retried per Max Retry Count in Mailbird Settings)
Status is synced automatically every minute by the sync_enquiry_statuses scheduler task.
Guest-accessible. Rate-limited to 5 requests per minute per IP.
Form fields:
| Field | Required | Description |
|---|---|---|
site |
Yes | Domain name matching a Website Master domain_name |
name |
Yes | Visitor name |
email |
Yes | Visitor email |
message |
Yes | Enquiry message |
phone |
No | Visitor phone |
subject |
No | Enquiry subject (defaults to Default Enquiry Subject in Mailbird Settings) |
cf_turnstile_response |
Conditional | Turnstile challenge token (when captcha type is Turnstile) |
g_recaptcha_response |
Conditional | reCAPTCHA token (when captcha type is reCAPTCHA) |
website_url |
N/A | Honeypot field; must be empty |
Success response:
{"message": {"success": true, "message": "Thank you! Your enquiry has been sent."}}Error response:
{"message": {"success": false, "error": "..."}}Two default templates are shipped as fixtures and synced on every bench migrate:
| Template Name | Used for |
|---|---|
Mailbird - Admin Notification |
Admin notification email |
Mailbird - Auto Reply |
Visitor receipt email |
{{ visitor_name }} : visitor's full name
{{ visitor_email }} : visitor's email address
{{ visitor_phone }} : visitor's phone (empty string if not provided)
{{ subject }} : enquiry subject
{{ message }} : enquiry message body
{{ website_name }} : Website Master display name
{{ site }} : domain name (e.g. example.in)
{{ initial }} : first letter of visitor_name (for monogram use)
- In the Frappe desk, open Email Template and click New (or duplicate an existing Mailbird template)
- Name it anything that does not start with
Mailbird -(e.g.MyClient - Admin Notification) so migrations do not overwrite it - Write the subject and HTML body using the variables above
- On the Website Master for that site, set Admin Notification Template or Auto Reply Template to the new template name
If a template field is left blank, the corresponding built-in template from api.py is used as the fallback.
When allowed_domains is saved on a Website Master, a background job rebuilds allow_cors in site_config.json:
- Add a domain: appears in
allow_corswithin seconds - Remove a domain: removed from
allow_corson next save; other sites' domains are untouched - Deactivate a Website Master: its domains are removed automatically
- Manually added entries: any domain in
allow_corsthat was not written by Mailbird (tracked via_mailbird_cors) is preserved permanently
Domain validation rules (enforced before save):
- Must be a valid hostname, IPv4, or
localhost, with optional port https://orhttp://scheme allowed; all other schemes rejected- No wildcards (
*.example.com) - No paths, query strings, or fragments
- Port must be 1-65535
- Max 260 characters per line
- Duplicates are deduplicated automatically
| Task | Frequency | Purpose |
|---|---|---|
sync_enquiry_statuses |
Every ~1 min (all) |
Advances Website Enquiry status based on Email Queue outcomes |
retry_failed_enquiries |
Every 15 min | Re-queues admin emails that failed delivery (up to Max Retry Count) |
purge_old_records |
Daily | Deletes old error logs and enquiry records per Mailbird Settings retention values |
A Single DocType (one record per site) for app-wide configuration. Open it from the MailBird workspace sidebar.
| Setting | Default | Description |
|---|---|---|
| Max Retry Count | 3 | Times a failed admin notification email is retried before being marked permanent failure |
| Setting | Default | Description |
|---|---|---|
| Max Message Length | 3000 | Characters accepted in the message field; longer input is silently truncated |
| Default Enquiry Subject | "General Enquiry" | Subject used when the visitor does not supply one |
| Setting | Default | Description |
|---|---|---|
| Webhook Timeout (seconds) | 10 | Seconds to wait for a webhook endpoint before timing out |
| Setting | Default | Description |
|---|---|---|
| Auto-purge Mailbird Error Logs | On | Delete Mailbird Error Log records older than the retention period (runs daily) |
| Error Log Retention (days) | 30 | Age at which error log records are deleted |
| Auto-purge Website Enquiries | Off | Delete Website Enquiry records older than the retention period (for GDPR compliance) |
| Enquiry Retention (days) | 90 | Age at which enquiry records are deleted |
Mailbird captures a filtered copy of every Frappe Error Log entry that originates from Mailbird code into a dedicated Mailbird Error Log DocType. This keeps Mailbird errors separate from framework noise so they are easier to triage from the desk.
An error is captured when any of the following is true:
- The log title matches a known Mailbird title (e.g.
ADMIN EMAIL QUEUE FAILED,WEBHOOK DELIVERY FAILED) - The title contains
CAPTCHAorCORS - The traceback file path contains
/apps/mailbird/
| Field | Description |
|---|---|
| Error Title | Title passed to frappe.log_error |
| Error Type | Auto-classified: API / Email / Captcha / Webhook / CORS / Template / Scheduler / Other |
| Occurred At | Timestamp from the original Frappe Error Log |
| Function | Innermost function name from the traceback |
| Website Master | Linkable to a Website Master for filtered views (populated manually if needed) |
| Frappe Error Log | Link to the original entry for full context |
| Resolved | Checkbox to mark the error as handled |
| Resolution Note | Free-text field for the fix or root cause |
The list view has standard filters on Error Type, Website Master, and Resolved so on-call triage requires no custom reports.
The capture hook (after_insert on Error Log) swallows all exceptions silently to prevent infinite logging loops.
Built and maintained by Abhishek Chougule at Techbird.
MIT