TATER's email-intake pipeline turns every email a shared mailbox receives into a TATER Ops task. Attachments are stored, the requester gets an auto-reply with a ticket id, and replies stay threaded as comments on the original ticket. Per-org subject routing rules + VIP allowlist + sender allowlist let you control how things land.
How it works
- User sends an email to the shared mailbox (e.g.
IT@bletzer.com). - Microsoft Graph fires a webhook to TATER (within a few seconds).
- TATER fetches the message + attachments via Graph, runs the email pipeline (HTML→text, signature strip, quote trim, OOO filter, forward-detection, People lookup, sender allowlist, VIP override, routing rules), and creates a TaskerTask with attachments stored in Azure Blob.
- If auto-reply is enabled, TATER sends the requester an email from the same shared mailbox with
[TATER-XXXXXXXX]in the subject and a link to My TATER where they can see their reported tickets. - When the requester replies (keeping
[TATER-XXXXXXXX]in the subject), TATER appends the reply as a comment on the ticket - no duplicate ticket. - When an agent or admin posts a comment on the ticket, TATER emails the requester from the shared mailbox so they see the update in their inbox.
Prerequisites
- A shared mailbox in Microsoft 365 (e.g.
IT@bletzer.com,helpdesk@yourdomain.com). - Admin access in your Entra tenant: Application Administrator + Exchange Administrator, or Global Administrator.
- A user with OrgAdmin role in TATER (to configure the integration in Manage).
- PowerShell 7+ on the workstation running the setup script.
Step 1 - Provision the app registration via PowerShell
TATER ships a setup script that does the whole Entra side in one command - creates the app reg, adds the right Graph permissions, grants admin consent, generates a client secret, AND locks the app to ONLY your shared mailbox via an Exchange Online Application Access Policy. Without that last step, an app-only Graph permission grants the app access to every mailbox in the tenant.
Get the script
The script is published at a public URL - no TATER repo access required. This is the path to hand to a client or tenant admin. From a PowerShell 7+ session on a workstation able to sign into the target tenant:
irm https://www.tatersecurity.com/Setup-TATEREmailIntake.ps1 -OutFile Setup-TATEREmailIntake.ps1
.\Setup-TATEREmailIntake.ps1 -Mailbox Support@yourdomain.com
The script contains no secrets - it only provisions an app registration in the tenant the runner signs into and prints the resulting values to that runner's console. Alternatives:
- Direct download in a browser: https://www.tatersecurity.com/Setup-TATEREmailIntake.ps1
- From the TATER repo (requires TATER-Security access): Setup-TATEREmailIntake.ps1
- Or clone the repo locally and use the file in the root:
.\Setup-TATEREmailIntake.ps1
Run the script
cd C:\path\to\TATER
Set-ExecutionPolicy -Scope Process -ExecutionPolicy Bypass -Force
.\Setup-TATEREmailIntake.ps1 -Mailbox IT@bletzer.com
Defaults to device-code sign-in. You'll get a code + URL to visit in any browser. After Graph auth, you'll get a second sign-in prompt for Exchange Online (used only for the Application Access Policy).
Script parameters
| Parameter | Required | Default | Notes |
|---|---|---|---|
-Mailbox | Yes | - | Shared mailbox the app reg will be scoped to. Must already exist. |
-AppDisplayName | No | "TATER Email Intake" | Display name of the app registration. Idempotent - reuses existing reg with this name. |
-SecretLifetimeDays | No | 365 | Client secret expiry. Re-running the script generates a fresh secret each time. |
-UseBrowserAuth | No | (off - device code) | Force interactive browser auth instead of device code. Try this only if device code fails. |
What the script does (idempotent)
- Auto-installs required modules if missing:
Microsoft.Graph.Applications,Microsoft.Graph.Identity.DirectoryManagement,ExchangeOnlineManagement(CurrentUser scope). - Connects to Microsoft Graph with just the three scopes it needs:
Application.ReadWrite.All,AppRoleAssignment.ReadWrite.All,Directory.ReadWrite.All. - Finds or creates an app registration with the configured display name.
- Creates the matching service principal if missing.
- Adds three Microsoft Graph application permissions if not already present:
Mail.ReadWrite- read messages and mark them read / move to a folderMail.Send- auto-reply on ticket creation + outbound notifications for commentsMailboxSettings.ReadWrite- create the "TATER-Processed" subfolder for housekeeping
- Grants admin consent on each permission (no Azure portal click-through required).
- Generates a fresh client secret with the configured lifetime. Prints it once - copy immediately.
- Connects to Exchange Online.
New-ApplicationAccessPolicyrequires a Mail-Enabled Security Group (MESG) as the scope identity - passing a mailbox address directly fails with "The identity of the policy scope is not a security principal.". The script auto-creates a per-app MESG namedTATER-EmailIntake-<safeName>(e.g.TATER-EmailIntake-ITforIT@bletzer.com) containing the target mailbox as a member, then applies the policy to that group. - Runs two
Test-ApplicationAccessPolicycalls to verify propagation: a positive test against the target mailbox (must returnGranted) AND a negative test against the caller's own mailbox (must returnDenied). The negative test is the critical security check - if it returnsGranted, the policy didn't actually persist and the app has tenant-wide mailbox access. The script surfaces a loud warning in that case. - Prints a copy-paste block with the four values you'll paste into TATER Manage.
Emails never become tickets even though setup "succeeded" (Exchange RBAC for Applications / [RAOP])
Symptom: The setup script completed cleanly, Test-ApplicationAccessPolicy returns Granted, the Graph subscription is active in TATER Manage - yet inbound email is silently dropped: no ticket, no auto-reply, no error shown to the sender. The TATER API log shows [emailIntakeWebhook] message processing failed: Graph message fetch failed: 403, and a direct app-only read returns:
403 ErrorAccessDenied
"Access to OData is disabled: [RAOP] : Blocked by tenant configured AppOnly AccessPolicy settings."
Cause: The tenant has Exchange Online's RBAC for Applications enforcement turned on in restrict / default-deny mode. This blocks all app-only access to mailbox data unless the specific app has an explicit Exchange management-role assignment. It is a separate, newer mechanism from the ApplicationAccessPolicy the setup script applies - and Test-ApplicationAccessPolicy only checks the old model, which is why it misleadingly reports Granted while reads still 403. Every other signal looks healthy: the Graph Mail.ReadWrite permission is consented, the token issues fine, and the Graph subscription even creates successfully (subscription management is a pure Graph operation; only the mailbox read is proxied to Exchange and gated).
Fix - grant the app an Exchange application role, scoped to just the intake mailbox (Exchange Online PowerShell, as an Exchange Administrator):
# 1. Register the app's service principal in Exchange RBAC (skip if Get-ServicePrincipal already returns it)
$spId = (Get-MgServicePrincipal -Filter "appId eq '<your-app-id>'").Id # or: az ad sp show --id <your-app-id> --query id -o tsv
New-ServicePrincipal -AppId <your-app-id> -ObjectId $spId -DisplayName "TATER Email Intake"
# 2. Scope to just the intake mailbox (least privilege)
New-ManagementScope -Name "TATER-EmailIntake" -RecipientRestrictionFilter "PrimarySmtpAddress -eq 'IT@bletzer.com'"
# 3. Assign the two application roles, scoped
New-ManagementRoleAssignment -App <your-app-id> -Role "Application Mail.ReadWrite" -CustomResourceScope "TATER-EmailIntake"
New-ManagementRoleAssignment -App <your-app-id> -Role "Application Mail.Send" -CustomResourceScope "TATER-EmailIntake"
Application Mail.ReadWrite covers reading + the move-to-folder housekeeping; Application Mail.Send covers the auto-reply. Allow up to ~30 minutes for the assignment to propagate before the read clears.
Scope filter caveat: the filter must match the mailbox's primary SMTP address. If the org uses multiple vanity domains (e.g. bletzer.com and caronbletzer.com), confirm which is primary with Get-Mailbox <mbx> | fl PrimarySmtpAddress,Alias,EmailAddresses - or scope by the immutable alias instead: Set-ManagementScope -Identity "TATER-EmailIntake" -RecipientRestrictionFilter "Alias -eq 'IT'".
Adding more mailboxes later: this enforcement is tenant-wide, so every additional intake mailbox needs the same grant. Broaden the management scope (e.g. scope to a security group and add each mailbox to it) rather than creating a fresh assignment per mailbox.
Common PowerShell errors
| Error | Cause | Fix |
|---|---|---|
Method not found: 'Microsoft.Identity.Client.BaseAbstractApplicationBuilder`1.WithLogging' |
MSAL DLL version mismatch - older Microsoft.Graph modules loaded alongside newer dependencies in the same PS session. |
Quickest: close PS completely, open a fresh PowerShell 7 session, and either:
Workaround if you can't update: the script also accepts -UseBrowserAuth which bypasses the InteractiveBrowserCredential MSAL path entirely and uses the legacy browser dialog flow: .\Setup-TATEREmailIntake.ps1 -Mailbox IT@bletzer.com -UseBrowserAuth
|
| Script hangs at "Connecting to Microsoft Graph (device code sign-in)…" with no code printed | Older script versions piped Connect-MgGraph | Out-Null which silently suppressed the device code prompt. |
Ctrl+C to abort, git pull to get the fixed script, then re-run. Alternatively use -UseBrowserAuth to skip the device code flow entirely. |
Cannot be loaded because running scripts is disabled on this system |
PowerShell execution policy. | Set-ExecutionPolicy -Scope Process -ExecutionPolicy Bypass -Force (only affects this PS session). |
Mailbox '...' not found |
Mailbox doesn't exist yet, OR you're not signed in as Exchange Administrator. | Create the shared mailbox first via Exchange Admin Center, then re-run. Confirm your account has the Exchange Administrator role. |
The identity of the policy scope is not a security principal. |
You ran an old version of the script (or the equivalent commands manually) and passed a mailbox address directly as -PolicyScopeGroupId. New-ApplicationAccessPolicy requires a Mail-Enabled Security Group (or M365 group). The policy is NOT created and the app retains tenant-wide mailbox access. |
Either git pull and re-run the updated script (creates the MESG automatically), or fix manually:
Verify with: Get-ApplicationAccessPolicy | Where-Object { $_.AppId -eq '<your-app-id>' } - should return a row with IsValid: True.
|
Negative Test-ApplicationAccessPolicy fails with "The operation couldn't be performed because object '<UPN>' couldn't be found" |
The script tries to use the signed-in account's UPN as the negative test identity, but the UPN doesn't match a real mailbox primary SMTP. Common when admins have a UPN like jmiles-a@bletzer.com but their actual mailbox is jmiles@bletzer.com. |
Not a real failure - run the negative test manually against any other mailbox you know exists:Test-ApplicationAccessPolicy -Identity creigle@bletzer.com -AppId <your-app-id>Should return Denied for proof. |
Script reports ✓ Access policy applied but Graph subscription requests still touch other mailboxes |
Older script versions printed the success message even when New-ApplicationAccessPolicy failed (the cmdlet writes errors as non-terminating). If you ran an early-June-2026 version of the script, double-check the policy actually persisted. |
Get-ApplicationAccessPolicy | Where-Object { $_.AppId -eq '<your-app-id>' }. If no row returns, the policy was never created - apply it manually (see the row above). |
Insufficient privileges to complete the operation |
Account lacks Application Administrator role. | Use a Global Administrator account, or have a Global Admin grant the Application Administrator role to your account first. |
| Exchange Online connect skipped warning | You're a Global / Application Admin but not Exchange Administrator. App reg + permissions + secret are created, BUT the per-mailbox access policy isn't applied - the app currently has tenant-wide mailbox access. | Have an Exchange Admin run the manual remediation block the script printed. Then verify with Get-ApplicationAccessPolicy. |
TATER Manage Subscribe button: Subscription validation request failed. HTTP status code is 'BadRequest'. Notification endpoint must respond with 200 OK to validation request. |
You're running an older deployment of the TATER API that handled the validation handshake on GET. Microsoft Graph sends validation as POST. | Fixed in TATER as of 2026-06-05. If you're on a fresh deploy and still see this, contact TATER support - possible network/proxy interference between Graph and the webhook endpoint. |
TATER Manage Subscribe button: Email intake disabled for this organization |
You saved the config without ticking Enable email intake at the top of the form. The subscribe endpoint refuses to create a Graph subscription when the org's config flag is false. | Click ↻ Refresh, tick Enable email intake, click 💾 Save, then 📡 Subscribe. |
Setup succeeded and the subscription is active, but inbound email silently produces no tickets (no error to the sender; API log shows Graph message fetch failed: 403 / [RAOP]) |
The tenant enforces Exchange RBAC for Applications (default-deny on app-only mailbox access). The setup script only applies the older ApplicationAccessPolicy, so Test-ApplicationAccessPolicy reports Granted while reads still 403. |
Grant the app an Exchange application role assignment scoped to the mailbox - see Emails never become tickets even though setup "succeeded" above. |
Step 2 - Configure in TATER Manage
Open TATER Manage → Connections → Email-to-Ticket and fill in the form using the values the script printed.
Required fields
| Field | What to enter |
|---|---|
| Enable email intake | Tick this. |
| Shared mailbox address | The mailbox the app reg was scoped to (e.g. IT@bletzer.com). |
| Graph Tenant ID | Printed by the script. |
| App (Client) ID | Printed by the script. |
| Client Secret | Paste the secret value printed by the script. Stored encrypted at rest; redacted on subsequent reads. |
Optional fields
| Field | What it does |
|---|---|
| Default category / priority | Applied to every new ticket unless a routing rule overrides them. |
| Sender allowlist (one domain per line) | If non-empty, only emails from senders matching one of these domains create tickets. Empty list = accept all. Strongly recommended to keep this set so the shared mailbox can't be used as a free helpdesk-DDoS target. Add caronbletzer.com AND bletzer.com if you have users in both. |
| VIP allowlist (one email per line) | These senders get auto-flagged as High priority regardless of subject. Useful for execs / specific MSP customer contacts. |
| Subject routing rules | One per line, format regex || category || priority || assignee. First match wins. Invalid regex are silently skipped. Trailing fields can be blank.Example: (critical|urgent) || IT || High || helpdesk@bletzer.com |
| Move processed messages to a folder | When ticked, ingested messages get marked read and moved to the named folder (default TATER-Processed). Folder auto-creates on first use. |
| Strip phrases from body | One phrase per line. Lines containing any phrase are dropped from the ticket body BEFORE signature trimming. Useful for security banners ("TRUSTED SENDER...", "CAUTION: External email", "[EXTERNAL]"), DLP scan disclaimers, and other boilerplate that gets injected by the sender's tenant. Two syntaxes: plain text (case-insensitive substring match) or /regex/flags. Examples:TRUSTED SENDERCAUTION: External email/^Banner:.*$/i |
| Send auto-reply on new ticket | When ticked, the requester receives an HTML auto-reply from the same mailbox with a [TATER-XXXXXXXX] subject tag and a link to My TATER. |
Body cleanup pipeline
When an email arrives, TATER cleans the body in this order before saving it as a ticket description or comment:
- HTML strip. The HTML body is converted to plain text (scripts/styles dropped, block tags become newlines, entities decoded).
- Custom
stripPatterns(the field above). Each line matching a configured pattern is removed entirely. Runs of 3+ blank lines that the strip leaves behind are collapsed. - Built-in signature trim. Removes:
- RFC 3676
--delimiter (everything after). - Common mobile signatures ("Sent from my iPhone", "Get Outlook for iOS", etc.) and everything after.
- Confidentiality / disclaimer / legal notice boilerplate and everything after.
- Individual lines like
Task ID: task-XXXXXXXX(internal id leakage from prior auto-replies) and[TATER-XXXXXXXX]on its own line. These are line-level filters (drop only that line) because they can appear inside quoted reply chains.
- RFC 3676
- Quoted-reply trim (replies only). When the email is a reply to a
[TATER-XXXXXXXX]auto-reply, TATER strips the "On <date>, <name> wrote:" blocks and>-prefixed quoted lines so the comment only contains the new content.
Tip: when you see banner text in a freshly-created ticket that you'd rather not see, just add it as one line in the Strip phrases from body textarea and save. Changes apply on the next inbound email - no restart needed.
Save and Subscribe
Click 💾 Save, then 📡 Subscribe. The subscribe button calls Microsoft Graph to create a webhook subscription pointing at TATER's webhook receiver. A status banner at the top of the form shows the subscription id, expiry timestamp, and remaining hours. TATER renews the subscription automatically every 24h (Graph caps subscriptions at ~3 days).
Step 3 - Test end-to-end
Once the subscribe banner shows green, run through this six-test recipe to confirm every part of the pipeline works. Most issues surface on tests 1 and 4 (basic ingest + allowlist enforcement).
Test 1: Basic ticket creation
- From any mailbox in an allowed-sender domain, send an email to the configured shared mailbox:
- Subject:
Test ticket - please ignore - Body: a few lines of text
- Attach a small file (image and/or PDF are good)
- Subject:
- Within ~10 seconds, expect:
- A new task in TATER Ops with the email subject as title, body as description, and 📎 attachment chips on the task detail page (click to download via 1-hour SAS).
- An auto-reply in the sender's inbox with
[TATER-XXXXXXXX]in the subject (only if you enabled auto-reply). - The task on the sender's My TATER → Reported by me tab.
Test 2: Reply threading
- Reply to the auto-reply you got in Test 1. Keep the
[TATER-XXXXXXXX]tag in the subject - that's how TATER routes the reply back to the existing ticket. - Expect: a new comment appended to the existing ticket, NOT a new ticket. Verify by opening the task detail in TATER Ops and looking at the comments section.
Test 3: Outbound comment notification
- Open the test ticket in TATER Ops as an agent (your TATER admin account).
- Post a comment.
- Expect: an email lands in the original requester's inbox from the shared mailbox with the comment body inline and a link to My TATER.
- The requester can reply to that email (keeping the
[TATER-XXXXXXXX]tag) and the reply lands as another comment - closing the loop.
Test 4: Allowlist enforcement
- From an address NOT on your sender allowlist (e.g. a personal Gmail address), email the shared mailbox.
- Expect: silently dropped. No task, no auto-reply.
- Verify in TATER Manage → Activity Log with the channel filter set to
email-intake. There should be NO new entry for that message. If you DO see a task created, the sender's domain matches an allowlist entry you forgot about.
Test 5: VIP priority override (only if configured)
- Send a test email from a sender in your VIP allowlist.
- Expect: task created with priority
High, regardless of the subject content.
Test 6: Subject routing rules (only if configured)
- Send a test email with a subject matching one of your routing rule patterns (e.g. subject contains
urgent). - Expect: task lands in the configured category / priority / assignee from the routing rule, overriding the org defaults.
If a test fails
| Symptom | First thing to check |
|---|---|
| No task appears after ~30 seconds | Subscription status banner - is it still green? If expired, click Subscribe again. If green but no task, check Activity Log filtered to via: email-intake - silent drops (allowlist mismatch, OOO detection, duplicate) ARE logged with the reason in metadata. |
| Task appears but description is HTML soup | This shouldn't happen - the pipeline does HTML→text via htmlToText(). Check the actual stored description via the API or task detail page. If raw HTML survives, please report. |
| Auto-reply doesn't arrive | Did you tick "Send auto-reply on new ticket"? Save again if not. If it's ticked, verify the app reg has the Mail.Send permission granted (the script adds it but a manual setup might have skipped it). |
| Reply creates a NEW ticket instead of threading | The reply's subject probably lost the [TATER-XXXXXXXX] tag - some email clients strip "auto-reply"-style subjects. Open the auto-reply, click Reply, manually confirm the subject contains the tag, then send. |
| Attachments missing from the task | Check the attachment policy gates - exe / video files / files over 25 MB are silently rejected per security policy. The Activity Log entry shows the rejected attachment names in metadata. |
Multiple departments (HR / AP / AR / etc.)
TATER supports multiple monitored mailboxes per organization - IT, HR, AP, AR can each have their own integration with their own routing rules, allowlist, VIPs, and auto-reply settings. One Entra app registration services all of them. Adding a department is two short steps.
Step 1: Add the mailbox to the access policy group
Run the setup script in -AddMailbox mode for each new department mailbox:
.\Setup-TATEREmailIntake.ps1 -AddMailbox HR@bletzer.com
.\Setup-TATEREmailIntake.ps1 -AddMailbox AP@bletzer.com
.\Setup-TATEREmailIntake.ps1 -AddMailbox AR@bletzer.com
The script reuses the existing app registration, looks up the Application Access Policy already in place, and adds the new mailbox as a member of the scope group. No new app reg, no new secret, no new policy. Takes about 30 seconds per mailbox.
Step 2: Add the integration in TATER Manage
For each new mailbox, in TATER Manage → Email-to-Ticket:
- Click + Add Mailbox Integration.
- Set Display name (e.g. "HR", "Accounts Payable") and Mailbox (e.g.
HR@bletzer.com). - Tick Enable this integration.
- Set per-department defaults: category, priority, sender allowlist, VIPs, subject routing rules, auto-reply toggle.
- Click 💾 Save, then on the same integration click 📡 Subscribe.
You'll see the new integration row in the list immediately with the subscription status banner.
Why this works (architecture)
- Graph subscriptions are scoped to a specific resource (
/users/{mailbox}/mailFolders('inbox')/messages). Each mailbox needs its own subscription, but subscriptions are owned by the app reg - one app reg can hold N subscriptions across N mailboxes. - Application Access Policy is scoped to a Mail-Enabled Security Group. Just add each new mailbox as a member of the existing group.
- clientState is per-subscription (per-integration), so the webhook receiver disambiguates which mailbox each notification belongs to. Routing rules, VIPs, allowlist, and auto-reply settings are stored per-integration so each department gets its own behavior.
- Comment notifications from agents go out from the same mailbox the original message came in on - an HR ticket update is sent from
HR@bletzer.com, an AP ticket fromAP@bletzer.com, etc. Requester replies thread back to the right ticket via the[TATER-XXXXXXXX]subject tag.
Ongoing management
Rotating the client secret
Re-run Setup-TATEREmailIntake.ps1 -Mailbox IT@bletzer.com. It detects the existing app reg and generates a fresh secret. Paste the new value into TATER Manage and click Save.
Adding or changing a monitored mailbox
The Application Access Policy is one-mailbox-per-app for the cleanest scope. If you need to change which mailbox TATER monitors, either:
- Run the script again with
-Mailbox NewAddress@yourdomain.comAND-AppDisplayName "TATER Email Intake - NewAddress"to create a separate app reg, OR - Manually
Remove-ApplicationAccessPolicyfor the old mailbox andNew-ApplicationAccessPolicyfor the new one in Exchange Online PowerShell.
Audit trail
Every email-intake ticket creation and every reply→comment append is logged in TATER Manage → Activity Log with via='email-intake'. Filter the channel dropdown to email-intake to see only this pipeline's activity.
Security notes
- Client secret encrypted at rest via AES-256-GCM (
ENCRYPTION_KEYenv var). Redacted to[REDACTED]on GET. Only the original Admin who set it can read it back. - Application Access Policy restricts the app reg to ONLY the shared mailbox you configured. Without this policy, Mail.ReadWrite and Mail.Send grant access to every mailbox in the tenant.
- Webhook clientState is a 32-byte random string generated on first save. Microsoft Graph echoes it on every notification; TATER constant-time compares it before processing.
- OOO loop detection drops any inbound message whose headers contain
Auto-Submitted,X-Auto-Response-Suppress,Precedence: bulk, etc. - prevents vacation-autoresponder storms from creating tickets. - Attachment policy: per-file 25 MB, per-task 100 MB, executables (
.exe .scr .ps1 .bat .msi .cmd .vbsetc.) and video files (video/*MIME + common extensions) rejected outright. - Sender allowlist (per-org) prevents the shared mailbox from being abused as a public ticket-creation endpoint. Keep this set whenever practical.
Related
- Task Notifications Setup - send a Teams card or email when new tickets land
- Public Intake Portal - alternative customer-facing intake
- Collaboration · Task Notifications overview