← Help & Docs

Email-to-Ticket Setup

Route inbound emails from a shared mailbox into TATER Ops tasks via Microsoft Graph. Last updated 2026-06-12

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

  1. User sends an email to the shared mailbox (e.g. IT@bletzer.com).
  2. Microsoft Graph fires a webhook to TATER (within a few seconds).
  3. 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.
  4. 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.
  5. When the requester replies (keeping [TATER-XXXXXXXX] in the subject), TATER appends the reply as a comment on the ticket - no duplicate ticket.
  6. 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

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:

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

ParameterRequiredDefaultNotes
-MailboxYes-Shared mailbox the app reg will be scoped to. Must already exist.
-AppDisplayNameNo"TATER Email Intake"Display name of the app registration. Idempotent - reuses existing reg with this name.
-SecretLifetimeDaysNo365Client secret expiry. Re-running the script generates a fresh secret each time.
-UseBrowserAuthNo(off - device code)Force interactive browser auth instead of device code. Try this only if device code fails.

What the script does (idempotent)

  1. Auto-installs required modules if missing: Microsoft.Graph.Applications, Microsoft.Graph.Identity.DirectoryManagement, ExchangeOnlineManagement (CurrentUser scope).
  2. Connects to Microsoft Graph with just the three scopes it needs: Application.ReadWrite.All, AppRoleAssignment.ReadWrite.All, Directory.ReadWrite.All.
  3. Finds or creates an app registration with the configured display name.
  4. Creates the matching service principal if missing.
  5. Adds three Microsoft Graph application permissions if not already present:
    • Mail.ReadWrite - read messages and mark them read / move to a folder
    • Mail.Send - auto-reply on ticket creation + outbound notifications for comments
    • MailboxSettings.ReadWrite - create the "TATER-Processed" subfolder for housekeeping
  6. Grants admin consent on each permission (no Azure portal click-through required).
  7. Generates a fresh client secret with the configured lifetime. Prints it once - copy immediately.
  8. Connects to Exchange Online. New-ApplicationAccessPolicy requires 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 named TATER-EmailIntake-<safeName> (e.g. TATER-EmailIntake-IT for IT@bletzer.com) containing the target mailbox as a member, then applies the policy to that group.
  9. Runs two Test-ApplicationAccessPolicy calls to verify propagation: a positive test against the target mailbox (must return Granted) AND a negative test against the caller's own mailbox (must return Denied). The negative test is the critical security check - if it returns Granted, the policy didn't actually persist and the app has tenant-wide mailbox access. The script surfaces a loud warning in that case.
  10. 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

ErrorCauseFix
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:
  1. Update-Module Microsoft.Graph -Force - works if Graph was installed via Install-Module
  2. Install-Module Microsoft.Graph -Force -AllowClobber -Scope CurrentUser - use this if step 1 returns "Module was not installed by using Install-Module, so it cannot be updated" (Graph was bundled with PowerShell or installed by another tool)
Then re-run the script.
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:
Connect-ExchangeOnline
New-DistributionGroup -Name "TATER-EmailIntake-IT" -Type Security `
  -Members @("IT@bletzer.com") `
  -PrimarySmtpAddress "tater-emailintake-it@bletzer.com"
New-ApplicationAccessPolicy -AppId <your-app-id> `
  -PolicyScopeGroupId "tater-emailintake-it@bletzer.com" `
  -AccessRight RestrictAccess
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

FieldWhat to enter
Enable email intakeTick this.
Shared mailbox addressThe mailbox the app reg was scoped to (e.g. IT@bletzer.com).
Graph Tenant IDPrinted by the script.
App (Client) IDPrinted by the script.
Client SecretPaste the secret value printed by the script. Stored encrypted at rest; redacted on subsequent reads.

Optional fields

FieldWhat it does
Default category / priorityApplied 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 rulesOne 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 folderWhen ticked, ingested messages get marked read and moved to the named folder (default TATER-Processed). Folder auto-creates on first use.
Strip phrases from bodyOne 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 SENDER
CAUTION: External email
/^Banner:.*$/i
Send auto-reply on new ticketWhen 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:

  1. HTML strip. The HTML body is converted to plain text (scripts/styles dropped, block tags become newlines, entities decoded).
  2. 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.
  3. 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.
  4. 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

  1. 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)
  2. 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

  1. 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.
  2. 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

  1. Open the test ticket in TATER Ops as an agent (your TATER admin account).
  2. Post a comment.
  3. 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.
  4. 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

  1. From an address NOT on your sender allowlist (e.g. a personal Gmail address), email the shared mailbox.
  2. Expect: silently dropped. No task, no auto-reply.
  3. 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)

  1. Send a test email from a sender in your VIP allowlist.
  2. Expect: task created with priority High, regardless of the subject content.

Test 6: Subject routing rules (only if configured)

  1. Send a test email with a subject matching one of your routing rule patterns (e.g. subject contains urgent).
  2. Expect: task lands in the configured category / priority / assignee from the routing rule, overriding the org defaults.

If a test fails

SymptomFirst 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:

  1. Click + Add Mailbox Integration.
  2. Set Display name (e.g. "HR", "Accounts Payable") and Mailbox (e.g. HR@bletzer.com).
  3. Tick Enable this integration.
  4. Set per-department defaults: category, priority, sender allowlist, VIPs, subject routing rules, auto-reply toggle.
  5. 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)

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:

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