IT Support & Help Desk Fundamentals Module 14 – Scripting, cloud and email support
Email, calendars and meeting apps
Match mail ports to protocols, fix apps that keep asking for a password, read SPF, DKIM and DMARC results, and sort calendar, camera and microphone faults.
What you will learn
- Explain which protocol and port a mail app uses to send mail and to read it
- Fix a mail app that keeps asking for a password now that Basic authentication is off
- Read SPF, DKIM and DMARC results to explain why a message went to junk
- Diagnose meetings that move after a clock change, and mail sent from shared mailboxes
- Troubleshoot camera, microphone and screen-sharing faults in meeting apps
Before you start
What you’ll learn · Before you start
What you will learn
- Explain which protocol and port a mail app uses to send mail and to read it
- Fix a mail app that keeps asking for a password now that Basic authentication is off
- Read SPF, DKIM and DMARC results to explain why a message went to junk
- Diagnose meetings that move after a clock change, and mail sent from shared mailboxes
- Troubleshoot camera, microphone and screen-sharing faults in meeting apps
Before you start
On this page
A quote sent from a new billing service lands in every customer’s junk folder, while a reply the sales team sent the same morning reaches the inbox. Both messages carry the answer in their headers: one server waited 11 minutes 42 seconds, and one check said fail.
Mail and meeting tickets look personal (“my email is broken”, “they cannot see me”) and are mostly read off a few fixed places. You will name the ports a mail app uses, fix an app that keeps asking for a password, read the results a receiving server writes into a message, explain why DMARC failed for the quote and what fixes it, and work out why a weekly meeting moved by an hour and why a camera tile stays black.
Mail goes out on one protocol and is read with another
A user setting up mail in a desktop app meets two servers: one to read mail from, one to send it through, each with a port number. They are different jobs. Sending from an app to its own mail service is message submission, which RFC 6409 puts on port 587; servers relaying mail to each other keep using SMTP on port 25. Reading uses IMAP or POP3. The IANA registry lists the ports a support desk meets:
| Port | IANA service name | Used for |
|---|---|---|
| 25 | smtp | servers relaying mail to each other |
| 587 | submission | an app sending, then switching to TLS |
| 465 | submissions | an app sending, TLS from the first byte |
| 993 | imaps | an app reading a mailbox over IMAP |
| 995 | pop3s | an app downloading mail over POP3 |
On 587 the connection opens in clear text and the app asks the server to start encryption with A command that upgrades a connection which began in clear text to an encrypted TLS one, before anything private is sent. Mail apps use it on the submission port 587.. On 465, 993 and 995 TLS starts as soon as the connection opens, which RFC 8314 calls TLS that starts the moment a connection opens, on a port used only for encrypted connections, such as 465 for sending mail and 993 and 995 for reading it., and the RFC recommends it in preference to STARTTLS for sending and reading mail. Not every app uses these ports: Microsoft’s Basic authentication page says phones’ built-in mail apps generally reach Exchange Online with Exchange ActiveSync, and lists Outlook for Windows and for Mac as connections of their own. Follow one message on its way:
The drawing shows the whole scene; its 4 steps are listed below.
Every step as text
- Submit, signed in. The app connects to its own mail service on port 465, where TLS starts with the first byte, or on 587, where it asks for STARTTLS before anything else. It signs in, then hands the message over.
- Sign it and relay it. The sending service may add a DKIM signature for its domain, then relays the message over SMTP on port 25 to the service that receives mail for the recipient's domain.
- Three checks. The receiver checks with SPF whether the server that connected may send for the envelope sender's domain, verifies any DKIM signature, then asks whether a domain that passed matches the From address. That last question is DMARC.
- The verdict, written down. It writes the three results into an Authentication-Results header at the top of the message, then files it in the inbox, or in junk when DMARC fails and the From domain's policy asks for that.
The last two steps are the ones a junk-mail ticket turns on, and the next three sections read them in detail. IANA’s registry, in the references, names every other port a settings screen may show.
Checkpoint
Check: Mail protocols and their ports
A password prompt that keeps coming back means Basic authentication
A user’s desktop mail app, set up years ago with IMAP and a password, asks for the password every few minutes, and typing the right one changes nothing. The app is using Signing in by sending the user name and password with every request, which apps often store on the device. Exchange Online no longer accepts it for mail protocols., and Microsoft’s deprecation page is plain about where that stands: it is now disabled in every Exchange Online tenant, and neither the tenant’s admins nor Microsoft support can switch it back on. It was removed for Exchange ActiveSync, POP, IMAP, Remote PowerShell, EWS, the Offline Address Book, Autodiscover and Outlook for Windows and Mac. SMTP AUTH: Sending mail through a server after signing in with a mailbox's credentials. Microsoft calls its form in Exchange Online client SMTP submission. More in “Add, share and secure printers and scanners” was switched off in tenants that were not using it, and Microsoft has announced plans to retire Basic authentication for it too, pointing to its latest announcement for the timeline.
What replaces the password is modern authentication: Signing in to a service with short-lived tokens from an identity provider, using OAuth 2.0, instead of sending the user name and password to the service itself. It also lets an organisation ask for a second factor. More in “Support phones and tablets from setup to sync”: the app opens a sign-in page, the user signs in there, with multifactor authentication where the organisation asks for it, and the app keeps a token. Microsoft added OAuth 2.0 to POP, IMAP and SMTP AUTH and names Thunderbird among the apps updated to use it, so the same desktop app often needs only a new version and a changed setting. The same page notes that app passwords stopped working too, for apps that do not support two-step verification. Work the fix from the cheapest step:
The drawing shows the whole scene; its 6 steps are listed below.
Every step as text
- Name the app first. Write down the app, its version and how the account was set up: a phone's built-in mail, a desktop app with IMAP, an old program that sends with SMTP. The same symptom has a different fix in each.
- Known to lack OAuth. An app whose maker says it cannot sign in with OAuth 2.0 will never stop asking, because the password route is gone. Skip straight to a replacement.
- Update it. Microsoft notes that the built-in mail apps of popular platforms typically support modern authentication, so the first fix is often the latest version of the app.
- Remove and add back. An app that is current but still set up the old way keeps sending the password. Removing the account and adding it back lets it discover the modern sign-in.
- Modern sign-in. Choose the account type or the OAuth 2.0 option the app offers. The sign-in then happens on a Microsoft page, with multifactor authentication, and the app keeps a token instead of the password.
- Still asking? Read the log. If the prompt comes back, the sign-in log of the account shows each attempt, the app that made it and any Conditional Access policy that refused it.
Organisations also refuse the old route on purpose. Microsoft recommends blocking sign-ins over legacy protocols that cannot do multifactor authentication with a Conditional Access: Microsoft Entra ID's policy engine that decides each sign-in from signals (user, device compliance, location, risk, application) after the password step. Each policy is an if-then rule with an allow, require MFA or block grant. More in “Cloud identity: Entra ID, SSO and device join” policy, and says more than 97 percent of credential stuffing attacks use legacy authentication. So a refused sign-in in the sign-in log: The record Microsoft Entra ID writes for every sign-in attempt, with the user, device, application and every Conditional Access policy that fired. A help-desk ticket about a cloud sign-in is often closed by reading one row of it. More in “Cloud identity: Entra ID, SSO and device join” is the organisation’s choice working. Phones have their own walk-through in the mobile devices lesson, and scanners that send mail in adding and sharing printers.
Asking for Basic authentication to be switched back on
Wrong: “Ask Microsoft to re-enable Basic for our tenant until the app is fixed.” Right: nobody can re-enable it any more, so update the app, add the account again with modern sign-in, or replace an app that cannot do OAuth 2.0.
Checkpoint
Check: The end of Basic authentication
The receiving server writes its verdict into the message
Back to the quote in junk. A message carries its own history in its headers. Every server that handles it adds a Received line at the top, which RFC 5322 calls a trace field, so the lines read from the bottom up give the route with a time at each step. And the receiving server writes what it decided into an A header that a receiving mail server adds to a message, recording the results of its checks, such as spf=pass, dkim=pass and dmarc=fail, under its own name. header, defined in RFC 8601, under its own name.
The program reads both: the sales reply and the billing quote, as samples invented for this lesson, sent to a customer whose server is mx.example.org. It works out each hop’s delay, picks the slowest, and reads the three results from the header mx.example.org added:
"""Read the trace of two messages: where each one waited, and what the receiving server decided about it.
Both messages are samples invented for this lesson, written in the header layout RFC 5322 and RFC 8601 define. The
sender's company is example.com, a billing service it has started to use is example.net, and the recipient's mail
server is mx.example.org. Every server that handles a message adds a Received line at the top, so the lines are read
from the bottom up. Only the Authentication-Results header added by the recipient's own server counts: the one that
names mx.example.org.
"""
from email.utils import parsedate_to_datetime
RECEIVER = "mx.example.org"
SALES_REPLY = """\
Authentication-Results: mx.example.org; spf=pass smtp.mailfrom=example.com; dkim=pass header.d=example.com; dmarc=pass header.from=example.com
Received: from out.example.com (out.example.com [192.0.2.25]) by mx.example.org with ESMTPS; Mon, 05 Oct 2099 10:00:07 +0530
Received: from laptop-07.example.com ([192.0.2.70]) by out.example.com with ESMTPSA; Mon, 05 Oct 2099 10:00:04 +0530
From: Sales <sales@example.com>
Subject: Your quote request"""
BILLING_QUOTE = """\
Authentication-Results: mx.example.org; spf=pass smtp.mailfrom=bounces.example.net; dkim=pass header.d=example.net; dmarc=fail header.from=example.com
Received: from mailer.example.net (mailer.example.net [198.51.100.40]) by mx.example.org with ESMTPS; Mon, 05 Oct 2099 10:14:52 +0530
Authentication-Results: mailer.example.net; dkim=pass header.d=example.net
Received: from queue.example.net ([198.51.100.41]) by mailer.example.net with ESMTP; Mon, 05 Oct 2099 10:03:10 +0530
Received: from app.example.net ([198.51.100.42]) by queue.example.net with ESMTP; Mon, 05 Oct 2099 10:03:08 +0530
From: Quotes <quotes@example.com>
Subject: Quote Q-1042"""
def header_lines(text, name):
"""The values of every header line called `name`, top first."""
prefix = name.lower() + ":"
return [line.split(":", 1)[1].strip() for line in text.splitlines() if line.lower().startswith(prefix)]
def hops(text):
"""(from host, by host, seconds since the previous hop) for each Received line, oldest first."""
found = []
for value in reversed(header_lines(text, "Received")):
route, when = value.rsplit(";", 1)
source = route.split("from ", 1)[1].split()[0]
target = route.split(" by ", 1)[1].split()[0]
found.append((source, target, parsedate_to_datetime(when.strip())))
out = []
for i, (source, target, when) in enumerate(found):
waited = 0 if i == 0 else int((when - found[i - 1][2]).total_seconds())
out.append((source, target, waited))
return out
def results(text, server):
"""method -> result, from the Authentication-Results header that `server` added."""
for value in header_lines(text, "Authentication-Results"):
authserv, _, rest = value.partition(";")
if authserv.strip() != server:
continue
found = {}
for part in rest.split(";"):
method, _, outcome = part.strip().partition("=")
found[method] = outcome.split()[0]
return found
return {}
for name, text in (("Sales reply", SALES_REPLY), ("Billing quote", BILLING_QUOTE)):
print(name)
trace = hops(text)
for source, target, waited in trace:
print(f" {source} -> {target}: {waited} s")
slow = max(trace, key=lambda hop: hop[2])
print(f" slowest hop: {slow[0]} -> {slow[1]}, {slow[2] // 60} min {slow[2] % 60} s")
found = results(text, RECEIVER)
print(f" {RECEIVER} says: spf={found.get('spf', 'none')} dkim={found.get('dkim', 'none')} dmarc={found.get('dmarc', 'none')}") Output
Sales reply laptop-07.example.com -> out.example.com: 0 s out.example.com -> mx.example.org: 3 s slowest hop: out.example.com -> mx.example.org, 0 min 3 s mx.example.org says: spf=pass dkim=pass dmarc=pass Billing quote app.example.net -> queue.example.net: 0 s queue.example.net -> mailer.example.net: 2 s mailer.example.net -> mx.example.org: 702 s slowest hop: mailer.example.net -> mx.example.org, 11 min 42 s mx.example.org says: spf=pass dkim=pass dmarc=fail
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 headers.py
Runs on this device, in your browser. The first run downloads Python (about 13.5 MB), which is kept for the next runs.
Your run, in this browser
The sales reply went through in 3 seconds and passed all three checks. The quote sat at mailer.example.net, the billing service’s last server, for 11 minutes 42 seconds before the customer’s server accepted it. Headers show where a message waited, not why, so the billing service’s own delivery log is the next place to ask. Then the verdict: Sender Policy Framework (RFC 7208): a domain's DNS record listing the servers allowed to send mail with that domain in the envelope sender, the bounce address a receiver checks. and DomainKeys Identified Mail (RFC 6376): a signature a sending service adds to a message for one domain, which receivers verify with a public key that domain publishes in DNS. passed, and Domain-based Message Authentication, Reporting and Conformance (RFC 9989): a check that passes only when the From address's domain matches a domain that SPF or DKIM confirmed, plus the policy its owner asks for when it fails. failed. Note the second Authentication-Results header in the quote, added by the billing service’s own server: the program ignores it, because only the receiving server’s word counts. RFC 8601 even tells a receiving server to delete any such header that carries its own name but arrived from outside, since anyone can write one.
Checkpoint
Check: Reading a message's route and results
DMARC passes only when a checked domain matches the From address
Two passes and a fail look contradictory until you see what each check covers. SPF and DKIM each confirm a domain, but not necessarily the domain in the From address the reader sees. DMARC joins them to the From address:
| Check | What it confirms | In the quote |
|---|---|---|
| SPF | the server may send for the envelope sender’s domain | pass, for bounces.example.net |
| DKIM | the signature is valid for the domain after d= |
pass, for example.net |
| DMARC | a confirmed domain aligns with the From domain | fail: the From is example.com |
RFC 9989 calls the match alignment. Under relaxed mode, the default, the confirmed domain and the From domain must share the same organisational domain, so bounces.example.com would align with example.com; strict mode needs them identical. Neither bounces.example.net nor example.net aligns with example.com, so DMARC failed although nothing was forged. The fix happens at the billing service: have it sign with a DKIM key for example.com, whose public key the company publishes in its own DNS, or send with an envelope sender under example.com, which that domain’s SPF record must then allow.
Key idea
SPF and DKIM can both pass for a sending service’s own domain while DMARC fails: DMARC passes only when a domain they confirmed aligns with the From address the reader sees.
Before the second fix, check what an include costs. The program reads sample TXT records of example.com as dig would print them: the SPF record before and after the service’s include is added, as it would be for quotes sent with an envelope sender at example.com itself, and the DMARC record:
"""Read a domain's SPF and DMARC records the way a technician checks them before changing anything.
The records are samples invented for this lesson, as `dig TXT example.com` and `dig TXT _dmarc.example.com` would print
them. For SPF, RFC 7208 lets the terms include, a, mx, ptr, exists and redirect cause at most 10 DNS lookups between
them; each include also brings the terms of the record it names, so the count of one record is a lower bound. For
DMARC, RFC 9989 requires v=DMARC1 first, treats a missing p as p=none, and ignores tags it does not define.
"""
LOOKUP_MECHANISMS = {"include", "a", "mx", "ptr", "exists"}
DMARC_TAGS = {"v", "p", "sp", "np", "psd", "t", "rua", "ruf", "adkim", "aspf", "fo"}
SPF_BEFORE = "v=spf1 include:spf.mail.example.org a mx ip4:192.0.2.25 -all"
SPF_AFTER = SPF_BEFORE.replace(" -all", " include:bounces.example.net -all")
DMARC = "v=DMARC1; p=quarantine; pct=50; rua=mailto:dmarc-reports@example.com"
def spf_lookups(record):
"""The terms of one SPF record that cost a DNS lookup."""
found = []
for term in record.split()[1:]:
name = term.lstrip("+-~?")
mechanism = name.split(":", 1)[0].split("/", 1)[0]
if mechanism in LOOKUP_MECHANISMS or name.startswith("redirect="):
found.append(term)
return found
def spf_all(record):
"""What the record says about every other sender: the qualifier of its all term."""
last = record.split()[-1]
return {"-all": "fail", "~all": "softfail", "?all": "neutral", "+all": "pass", "all": "pass"}.get(last, "no all term")
def dmarc_tags(record):
"""(tags RFC 9989 defines, tags it ignores) of one DMARC record."""
pairs = [part.strip().split("=", 1) for part in record.split(";") if part.strip()]
known = {k: v for k, v in pairs if k in DMARC_TAGS}
ignored = [k for k, _ in pairs if k not in DMARC_TAGS]
return known, ignored
for label, record in (("SPF now", SPF_BEFORE), ("SPF with the service added", SPF_AFTER)):
lookups = spf_lookups(record)
print(f"{label}: {record}")
print(f" lookup terms: {len(lookups)} of the 10 allowed ({', '.join(lookups)}); everyone else: {spf_all(record)}")
known, ignored = dmarc_tags(DMARC)
print(f"DMARC: {DMARC}")
print(f" policy for mail that fails: {known.get('p', 'none')}; test mode: {known.get('t', 'n')}")
print(f" aggregate reports to: {known.get('rua', 'nobody')}")
print(f" ignored, not defined by RFC 9989: {', '.join(ignored) or 'nothing'}") Output
SPF now: v=spf1 include:spf.mail.example.org a mx ip4:192.0.2.25 -all lookup terms: 3 of the 10 allowed (include:spf.mail.example.org, a, mx); everyone else: fail SPF with the service added: v=spf1 include:spf.mail.example.org a mx ip4:192.0.2.25 include:bounces.example.net -all lookup terms: 4 of the 10 allowed (include:spf.mail.example.org, a, mx, include:bounces.example.net); everyone else: fail DMARC: v=DMARC1; p=quarantine; pct=50; rua=mailto:dmarc-reports@example.com policy for mail that fails: quarantine; test mode: n aggregate reports to: mailto:dmarc-reports@example.com ignored, not defined by RFC 9989: pct
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 txt_records.py
Runs on this device, in your browser. The first run downloads Python (about 13.5 MB), which is kept for the next runs.
Your run, in this browser
RFC 7208 lets the terms that cost a DNS lookup (include, a, mx, ptr, exists and redirect) add up to at most 10, and each include brings the lookups of the record it names, so the program’s counts, 3 and then 4, are floors. The DMARC record asks receivers to quarantine failing mail, which is why the customers’ servers filed the quote in junk rather than refusing it. Under RFC 9989 its pct=50 means nothing: the tag was removed, unknown tags are ignored, and a t=y test mode took over part of its job.
Adding the service to the SPF record of the From domain
Wrong: “Add include:bounces.example.net to example.com’s SPF and the quotes will pass.” SPF is checked for the envelope sender’s domain, which is still the service’s own, so nothing aligns. Right: an aligned DKIM signature for example.com, or an envelope sender under example.com that its SPF allows.
Checkpoint
Check: SPF, DKIM and DMARC alignment
A recurring meeting keeps the clock of the zone it was made in
A London manager runs a weekly call at 09:00 London time with a team on UTC+05:30. One Monday in late October, the team say the call has moved an hour later. Nothing was edited. RFC 5545, the iCalendar standard calendars exchange, advises writing a recurring event’s start as a local time with its time zone, so that every instance starts at the same local time even when that zone’s clocks change. GOV.UK says the UK’s clocks go back an hour at 2am on the last Sunday in October, from British Summer Time (UTC+01:00) to Greenwich Mean Time (UTC+00:00), and forward again on the last Sunday in March. The program shows the same meeting from both sides, and the reverse case:
"""A weekly meeting across the week the UK's clocks go back, seen from London and from a UTC+05:30 office.
The offsets are written out instead of read from a time-zone database: London is UTC+01:00 on British Summer Time and
UTC+00:00 on Greenwich Mean Time (GOV.UK), and the other office stays on UTC+05:30. A recurring meeting keeps its
clock time in the time zone it was created in, so whoever is in the other zone sees it move.
"""
from datetime import datetime, timedelta, timezone
LONDON = {"week before": timezone(timedelta(hours=1)), "week after": timezone(timedelta(hours=0))}
OFFICE = timezone(timedelta(hours=5, minutes=30))
def shown(hour, minute, made_in, seen_in):
"""The meeting's local time in `seen_in`, when it is held at hour:minute in `made_in`."""
held = datetime(2000, 1, 3, hour, minute, tzinfo=made_in) # any day: only the clock time matters here
return held.astimezone(seen_in).strftime("%H:%M")
print("Organised in London for 09:00 London time:")
for week, london in LONDON.items():
print(f" {week}: London 09:00 (UTC{london.utcoffset(None).total_seconds() / 3600:+.0f}), office {shown(9, 0, london, OFFICE)}")
print("Organised in the office for 15:00 office time:")
for week, london in LONDON.items():
print(f" {week}: office 15:00, London {shown(15, 0, OFFICE, london)}") Output
Organised in London for 09:00 London time: week before: London 09:00 (UTC+1), office 13:30 week after: London 09:00 (UTC+0), office 14:30 Organised in the office for 15:00 office time: week before: office 15:00, London 10:30 week after: office 15:00, London 09:30
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 meeting_times.py
Runs on this device, in your browser. The first run downloads Python (about 13.5 MB), which is kept for the next runs.
Your run, in this browser
The London meeting stays at 09:00 for its organiser and moves from 13:30 to 14:30 for the team; a meeting created in the team’s office for 15:00 moves the other way for London, from 10:30 to 09:30. Neither is a fault. The fix, when it matters, is a decision about whose clock the meeting follows. One person seeing every meeting shifted, by contrast, usually has a device set to the wrong time zone, so compare the device’s zone and clock with the real local time before touching the calendar.
A delegate’s permission decides how mail from a shared mailbox looks
Mail sent for someone else has its own three permissions in Exchange Online, and Microsoft’s permissions page draws the lines a desk is asked about:
| Permission | What the delegate can do |
|---|---|
| Full Access | open the mailbox and work in it, but not send from it |
| Send As | send mail that appears to come from the mailbox itself |
| Send on Behalf | send as “delegate on behalf of mailbox”; replies go to the mailbox |
A delegate who holds both sending permissions always sends as Send As, Microsoft adds, which explains why “on behalf of” vanished after a second permission was granted.
Checkpoint
Check: Meeting times and mailbox permissions
A meeting app needs an app setting and an operating system permission
A user joins a call and their camera tile stays black, though the camera worked in the morning. Meeting faults sit in two layers: what the app has chosen, and what the operating system allows it. Check them in this order:
| Symptom | First checks |
|---|---|
| Black or missing video | the camera chosen in the app; the system’s camera permission |
| Others cannot hear you | mute, the microphone chosen in the app; the system’s microphone permission |
| Shared screen is blank, on a Mac | the app’s Screen & System Audio Recording permission |
| Headset sounds worse once the mic is on | the headset’s hands-free mode, peripherals lesson |
On Windows the camera switches sit under Settings, Privacy & security, Camera: Camera access for the device, then separate switches for Microsoft Store apps, one by one, and for desktop apps, all together, because Microsoft says desktop apps cannot be allowed one at a time. The microphone page has the same switches. When the device’s switch will not change, Microsoft says an administrator on the device has to change it. On a Mac, System Settings, Privacy & Security lists Camera, Microphone and Screen & System Audio Recording, each with a switch per app; the macOS permissions lesson explains the TCC: The name technicians use for the macOS privacy database, after the com.apple.TCC payload that manages it: the store of each user's decisions about which apps may reach the camera, the microphone, the screen and personal files. More in “macOS security: FileVault, SIP, permissions” database behind them. A call in a browser needs one more permission: Chrome asks each site for the camera and microphone, and at work an administrator may set those choices.
When the settings look right, test the hardware away from the meeting app, so you know which layer is left:
Webcam Test Check in the browser that the camera works and what resolution and frame rate it really sends; the picture stays on your device. Microphone Test See and hear whether the microphone works and how loud it is, with a short recording to play back; the sound is not uploaded.Checkpoint
Check: Camera, microphone and screen sharing
Practise: read a message’s results and its slowest hop
Most mail tickets come down to the two readings the headers program made. Write them as functions that take any message’s headers, ignore results written by the wrong server and read the route from the bottom up:
Exercise · Medium · Python
Read a message's results and its slowest hop
Write two functions that read the headers of one message, given as text with one header per line, the way the lesson's headers.py reads them.
auth_results(headers, server) returns a dict with exactly the keys "spf", "dkim" and "dmarc":
- read only the topmost
Authentication-Resultsheader whose first part, before the first;, isserver(compare the server names without regard to case), and ignore every header that another server added; - each value is the word after
method=, such aspass,failorsoftfail; - a method that header does not mention, and every method when no header from
serverexists, is"none".
slowest_hop(headers) returns (by_host, seconds) for the hop that took longest, or None when the message has fewer than two Received lines:
- every server adds its
Receivedline at the top, so read them from the bottom up; - the host after
bynames the server that added the line, and the date-time after the last;is when it did (email.utils.parsedate_to_datetimereads it); - a hop's time is the difference, in whole seconds, between a line and the one below it, and the hop belongs to the
byhost of the upper line; - when two hops take the same time, keep the older one, the one nearer the bottom.
The sample tests cover the lesson's billing quote, a header added by the sending service, a missing DKIM result and a server name in other case, a message with no results from the receiver, a result for a method other than the three, equal hops and a single Received line.
Starter code · read_headers.py
from email.utils import parsedate_to_datetime
def header_values(headers, name):
"""The values of every header line called `name`, top first."""
prefix = name.lower() + ":"
return [line.split(":", 1)[1].strip() for line in headers.splitlines() if line.lower().startswith(prefix)]
def auth_results(headers, server):
"""spf, dkim and dmarc results from the topmost Authentication-Results header that `server` added."""
# Trusts the first Authentication-Results header, whoever added it, and leaves missing methods out.
values = header_values(headers, "Authentication-Results")
found = {}
if values:
for part in values[0].split(";")[1:]:
method, _, outcome = part.strip().partition("=")
found[method] = outcome.split()[0]
return found
def slowest_hop(headers):
"""(by host, seconds) of the hop that took longest, or None with fewer than two Received lines."""
# Reads the Received lines top-down, so every gap comes out negative.
stamps = []
for value in header_values(headers, "Received"):
route, when = value.rsplit(";", 1)
stamps.append((route.split(" by ", 1)[1].split()[0], parsedate_to_datetime(when.strip())))
best = None
for i in range(1, len(stamps)):
seconds = int((stamps[i][1] - stamps[i - 1][1]).total_seconds())
if best is None or seconds > best[1]:
best = (stamps[i][0], seconds)
return best The sample tests · test_read_headers.py
from read_headers import auth_results, slowest_hop
QUOTE = """\
Authentication-Results: mx.example.org; spf=pass smtp.mailfrom=bounces.example.net; dkim=pass header.d=example.net; dmarc=fail header.from=example.com
Received: from mailer.example.net (mailer.example.net [198.51.100.40]) by mx.example.org with ESMTPS; Mon, 05 Oct 2099 10:14:52 +0530
Authentication-Results: mailer.example.net; dkim=pass header.d=example.net; dmarc=pass
Received: from queue.example.net ([198.51.100.41]) by mailer.example.net with ESMTP; Mon, 05 Oct 2099 10:03:10 +0530
Received: from app.example.net ([198.51.100.42]) by queue.example.net with ESMTP; Mon, 05 Oct 2099 10:03:08 +0530
From: Quotes <quotes@example.com>"""
NO_DKIM = """\
Authentication-Results: MX.example.org; spf=softfail smtp.mailfrom=example.com; dmarc=fail header.from=example.com
Received: from out.example.com ([192.0.2.25]) by mx.example.org with ESMTPS; Mon, 05 Oct 2099 09:00:30 +0530
Received: from pc-12.example.com ([192.0.2.71]) by out.example.com with ESMTPSA; Mon, 05 Oct 2099 09:00:00 +0530
From: Accounts <accounts@example.com>"""
EVEN = """\
Received: from b.example.net ([198.51.100.2]) by c.example.org with ESMTPS; Mon, 05 Oct 2099 11:00:20 +0530
Received: from a.example.net ([198.51.100.1]) by b.example.net with ESMTP; Mon, 05 Oct 2099 11:00:10 +0530
Received: from app.example.net ([198.51.100.3]) by a.example.net with ESMTP; Mon, 05 Oct 2099 11:00:00 +0530"""
def test_only_the_receiving_servers_header_counts():
assert auth_results(QUOTE, "mx.example.org") == {"spf": "pass", "dkim": "pass", "dmarc": "fail"}
def test_another_servers_header_is_read_only_when_asked_for():
assert auth_results(QUOTE, "mailer.example.net") == {"spf": "none", "dkim": "pass", "dmarc": "pass"}
def test_a_missing_method_reads_none_and_the_server_name_ignores_case():
assert auth_results(NO_DKIM, "mx.example.org") == {"spf": "softfail", "dkim": "none", "dmarc": "fail"}
def test_no_header_from_that_server():
assert auth_results(EVEN, "mx.example.org") == {"spf": "none", "dkim": "none", "dmarc": "none"}
def test_a_method_other_than_the_three_is_left_out():
extra = "Authentication-Results: mx.example.org; arc=pass; spf=pass smtp.mailfrom=example.com; dkim=pass header.d=example.com; dmarc=pass header.from=example.com"
assert auth_results(extra, "mx.example.org") == {"spf": "pass", "dkim": "pass", "dmarc": "pass"}
def test_slowest_hop_of_the_quote():
assert slowest_hop(QUOTE) == ("mx.example.org", 702)
def test_equal_hops_keep_the_older_one():
assert slowest_hop(EVEN) == ("b.example.net", 10)
def test_one_received_line_has_no_hop():
one = "Received: from a.example.net ([198.51.100.1]) by b.example.net with ESMTP; Mon, 05 Oct 2099 11:00:10 +0530"
assert slowest_hop(one) is None A hint
Start auth_results with every method set to "none", then look for the first header whose part before ; matches the server; split the rest on ; and each piece on the first =. For slowest_hop, reverse the list of Received values before you pair each line with the one before it, and replace the best hop only when a gap is strictly longer.
Results of the sample tests
| Test | Result | Details |
|---|
What your code printed
The sample tests run on this device, in your browser (Pyodide): nothing is sent to mysmartcopilot.com. The first run downloads Python (about 13.5 MB), which is kept for the next runs. A check in your browser is feedback for you, not proof that the code is right for every input.
Then check what you know, starting with the idea you are least sure of:
Practice that adapts to you
Practise this lesson
18 questions about 6 ideas of this lesson. Each answer is explained at once, and what you get right is kept in this browser.
Interview questions
Warm-up (fresher): a mail app’s settings offer port 465 or port 587 for sending. What is the difference?
Both are for message submission: an app handing outgoing mail to its own mail service after signing in. On 587, the submission port of RFC 6409, the connection starts in clear text and the app asks for STARTTLS to encrypt it before it signs in or sends anything. On 465, which IANA lists as submissions, TLS starts from the first byte, which RFC 8314 calls implicit TLS and recommends in preference to STARTTLS. Either is fine when the service supports it and the app is set to match: 465 with TLS or SSL, 587 with STARTTLS. Port 25 is the one not to use in an app, because it is for servers relaying mail to each other. Reading mail is a separate setting: IMAP on 993 or POP3 on 995.
The next two tickets need more than this lesson’s examples: a forwarding rule to a personal address that stopped overnight, and an old in-house tool built on Exchange Web Services. Note your first move on each before its page shows the opening step:
Model answers open with any pass: ₹79 for 1 week ($2.99), one payment that never renews.
-
Fresher to mid
A sales manager's inbox rule forwarded every message to a personal address outside the company. Overnight it stopped, and each forward now comes back as a bounce. The manager wants it working again today. What do you check, and what do you say?
-
Mid to senior
The finance team's old in-house tool reads invoices from a shared mailbox through Exchange Web Services. Its author has left, and a vendor tells you EWS is going away. How do you find everything that still uses EWS, and what does a lasting fix look like?
Flashcards · 7 cards
Recall it, then check
Say the answer to yourself before you look: recalling it is what makes it stick.
-
Which ports does a mail app use to send and to read mail?
Submission on 587 with STARTTLS or on 465 with implicit TLS; reading with IMAP on 993 or POP3 on 995. Port 25 is for servers relaying mail between themselves.
-
What is the difference between ports 465 and 587 for sending mail?
On 587 the connection starts in clear text and the app asks for STARTTLS before it signs in. On 465 TLS starts from the first byte, which RFC 8314 calls implicit TLS.
-
A mail app keeps asking for an Exchange Online mailbox's password, and the right one changes nothing. What is going on?
It is using Basic authentication, which Exchange Online no longer accepts and nobody can switch back on. Use a current app, add the account again with modern sign-in, or replace the app.
-
How do you read a message's route and find its slowest hop?
Read the Received lines from the bottom up: each server adds one with a time, so the biggest gap between two neighbouring lines is the slowest hop. Only the Authentication-Results header written by the receiving server counts.
-
SPF and DKIM both pass for a sending service, yet DMARC fails. Why?
DMARC passes only when a domain that SPF or DKIM confirmed aligns with the From domain. The service needs an aligned DKIM signature or envelope sender, not an extra include in the From domain's SPF record.
-
Why does a recurring meeting shift for people in another country?
It keeps the local time of the zone it was created in, so when that zone's clocks change, it moves by an hour for people whose own clocks stay put.
-
In what order are camera, microphone and screen-sharing faults checked?
The app's settings first, then the operating system's privacy permissions and the browser's site permissions, and the hardware on its own last.
Key takeaways
- Apps send mail by message submission on 587 with STARTTLS or 465 with implicit TLS, read it with IMAP on 993 or POP3 on 995, and leave port 25 to servers relaying mail between themselves.
- A mail app that keeps asking for a password is using Basic authentication, which Exchange Online no longer accepts and nobody can switch back on, so the fix is a current app, the account added again with modern sign-in, or a replacement.
- Received lines read from the bottom up give a message’s route and its slowest hop, and only the Authentication-Results header written by the receiving server counts.
- DMARC passes only when a domain that SPF or DKIM confirmed aligns with the From domain, so a sending service needs an aligned DKIM signature or envelope sender, not an extra include in the From domain’s SPF record.
- A recurring meeting keeps the local time of the zone it was created in, so it moves for people elsewhere when that zone’s clocks change, while Send As and Send on Behalf decide how mail from a shared mailbox looks.
- Camera, microphone and screen-sharing faults are checked in the app first, then in the operating system’s privacy permissions and the browser’s site permissions, and the hardware is tested on its own last.
References
- RFC 6409: Message Submission for Mail (RFC Editor (IETF))
- RFC 8314: Cleartext Considered Obsolete: Use of TLS for Email Submission and Access (RFC Editor (IETF))
- Service Name and Transport Protocol Port Number Registry (IANA)
- Deprecation of Basic authentication in Exchange Online (Microsoft)
- Authenticate an IMAP, POP or SMTP connection using OAuth (Microsoft)
- Block legacy authentication with Conditional Access (Microsoft)
- RFC 5322: Internet Message Format (RFC Editor (IETF))
- RFC 8601: Message Header Field for Indicating Message Authentication Status (RFC Editor (IETF))
- RFC 7208: Sender Policy Framework (SPF) (RFC Editor (IETF))
- RFC 6376: DomainKeys Identified Mail (DKIM) Signatures (RFC Editor (IETF))
- RFC 9989: Domain-Based Message Authentication, Reporting, and Conformance (DMARC) (RFC Editor (IETF))
- RFC 5545: Internet Calendaring and Scheduling Core Object Specification (iCalendar) (RFC Editor (IETF))
- When do the clocks change? (GOV.UK)
- Manage permissions for recipients in Exchange Online (Microsoft)
- Manage app permissions for your camera in Windows (Microsoft)
- Turn on app permissions for your microphone in Windows (Microsoft)
- Control access to the camera on Mac (Apple)
- Control access to the microphone on Mac (Apple)
- Control access to screen and system audio recording on Mac (Apple)
- Use your camera and microphone in Chrome (Google)
Related tools
Report a problem with this lesson
Kept only in this browser. Your Learn progress