Guide Automation

Outlook attachments saved automatically: three ways, no VBA

Outlook rules can't save attachments, and new Outlook has no VBA. Two free Microsoft tools and a 56-line script can, once you guard against three traps.

The short read

Saving every attachment under its own name lost two of three invoices in our 12-email test.

  • Outlook rules can't save attachments, and the new Outlook for Windows doesn't support VBA.
  • A free cloud flow, Power Automate for desktop or a 56-line Python script can, with an inline filter and unique file names.

What you can do with this

If your office uses Microsoft 365, build the cloud flow with an Is Inline filter and a date-plus-guid file name. New attachments then land in OneDrive without logos or overwrites.

Three stacked mesh trays of loose papers on a wooden desk, headphones hanging beside them and a phone in front.
Photo: Khairil Yusof, CC BY 2.0
In this article
  1. Why the rule and the macro are dead ends
  2. Choose the way that fits your Outlook
  3. Learn the three traps from 12 test emails
  4. 1. Build a cloud flow that saves each new attachment
  5. Watch the gaps Microsoft documents in the cloud flow
  6. 2. Sweep a whole folder with Power Automate for desktop
  7. 3. Run a script on messages saved as .eml files
  8. Check the folder after the first week

Outlook’s rules can move, flag, forward or delete a message, but none of their actions saves an attachment to a folder. The old fix was a VBA macro, and the new Outlook for Windows doesn’t support VBA. If you still drag invoices out of your inbox by hand, three tools without macros can do it for you.

They are a Power Automate cloud flow, Power Automate for desktop and a short Python script. None costs extra for an office already on Microsoft 365, and each fits a different version of Outlook. Each also faces the same three traps: signature logos that pile up as junk, files with the same name that collide, and names Windows refuses.

We built 12 sample emails with those traps in them and ran them through a naive save and through our script. The naive save quietly lost two of three invoices. Below are the results, the exact settings for each tool, and the limits Microsoft documents for them. All limits and license terms were read on October 19, 2026.

Why the rule and the macro are dead ends

Microsoft’s rules page lists what a rule can do: move a message to a folder, delete it, change its importance, flag it or forward it. Saving an attachment is not on the list. Classic Outlook can run extra rule actions only through third-party add-ins, and the page says “Microsoft doesn’t provide custom action add-ins”.

The answers you will find on old forums use a rule with the “Run a script” action, which calls a VBA macro. That action was switched off by a security update. An answer from Microsoft support staff on its Q&A forum explains how to bring it back with a registry value called EnableUnsafeClientMailRules, and the name tells you what Microsoft thinks of it. It also works only in classic Outlook, because the new Outlook for Windows runs neither VBA nor COM add-ins.

Our view: don’t build anything new on that route. It needs a security setting turned off on every PC that runs it, and it dies the day your company moves to the new Outlook.

Choose the way that fits your Outlook

The right tool depends on two facts: which Outlook you use, and whether your mailbox is a work account in Microsoft 365.

Cloud flow Power Automate for desktop Python script
Works with Microsoft 365 work or school mailbox, any Outlook version Classic Outlook on a Windows PC; not new Outlook Any Outlook that saves a message as an .eml file
Cost Free license for work and school accounts Free; personal Microsoft accounts can sign in Free; needs Python 3
Runs By itself when mail arrives When you start it When you start it
Saves to OneDrive A folder on the PC A folder on the PC
Best for New mail, from now on Clearing a full folder in one run Mac, new Outlook, the web version
Main limit Skips messages over 50 MB, or your admin’s lower limit No ARM PCs; scheduling from the cloud needs a premium license You save each message by hand first

Many offices will want two of these: the cloud flow for everything that arrives from today, and a one-off sweep for the backlog already sitting in the inbox.

Learn the three traps from 12 test emails

We built 12 sample emails in standard email format (.eml) with Python’s email library, covering what a real inbox throws at a “save every attachment” job.

Three vendors each sent a file called invoice.pdf. Two messages carried a signature logo called image001.png, and one held a pasted screenshot. One file name, Q3: "final" report?.pdf, used characters Windows refuses. The set also had a forwarded email with a contract inside, a resent duplicate, a Swedish file name with å, ä and ö, and a file called winmail.dat.

First we ran a naive script that saves every named attachment under its own name into one folder. It wrote 12 files, and 8 were left at the end.

  • Overwrites. Four writes of invoice.pdf left one file. The invoices from vendors B and C were gone, overwritten without a warning, and the survivor was vendor A’s resent copy.
  • Junk. Both signature logos were saved as image001.png, and the second overwrote the first. Logos with other names would each add a file.
  • Bad names. Our sandbox runs Linux, which accepted Q3: "final" report?.pdf. On Windows, the colon, the quotes and the question mark are all reserved characters, and the save would fail.

Then we ran our script, shown in step 3. It saved all 9 real attachments, including the contract inside the forwarded email. It skipped 3 inline images and 1 exact duplicate, and gave every file a name like 2026-10-02_vendor-b.com_invoice.pdf. A second run over the same emails saved nothing and reported 10 duplicates, so running it twice does no harm.

The skipped images include the pasted screenshot. That is the trade-off in the cloud flow and the script below: a filter that drops signature logos also drops images people paste into the body. If your team mails screenshots that matter, keep inline images and delete the logos later.

1. Build a cloud flow that saves each new attachment

This needs a Microsoft 365 work or school account. Power Automate’s free license covers it, because both the Office 365 Outlook and OneDrive for Business connectors are standard. The OneDrive connector needs a work or school account, not a personal one. We had no Microsoft 365 tenant in our sandbox, so the steps below come from Microsoft’s connector reference, not from a flow we ran.

  1. In Power Automate, create an automated cloud flow with the trigger When a new email arrives (V3) from Office 365 Outlook.
  2. Set Only with Attachments to Yes and Include Attachments to Yes. Narrow it with Folder, From or Subject Filter if only some mail matters, for example From: invoices@vendor.com.
  3. Add an Apply to each loop over the trigger’s Attachments.
  4. Inside the loop, add a Condition: Is Inline is equal to false. It does the same job as the logo filter in our script.
  5. Under If yes, add OneDrive for Business Create file. Pick a Folder Path such as /Email attachments, set File Content to the attachment’s Content, and set File Name to this expression:
concat(formatDateTime(utcNow(), 'yyyy-MM-dd'), '_', guid(), '_', item()?['name'])

The date is the day the flow ran, in UTC, and it sorts the folder. The guid() adds a randomly generated 36-character code, so two vendors’ invoice.pdf files can never land on the same name. It makes names long; that is the price of never losing a file. You don’t need to clean the name yourself, because Create file replaces characters OneDrive disallows with _.

Watch the gaps Microsoft documents in the cloud flow

The Office 365 Outlook connector reference lists these, and each one can leave a gap or a bad file in the folder:

  • The trigger “will skip any email that has a total message size greater than the limit put by your Exchange Admin or 50 MB, whichever is less”. It may also skip protected emails.
  • Forwarded emails attached to a message, calendar items and bounce reports are “not supported by the connector”. Attached .eml, .msg and .ics files appear only when they arrive inside a .zip. Our script handled the forwarded contract; this flow will not.
  • For digitally signed emails, the trigger output “may contain incorrect attachment content”.
  • With Include Attachments set to Yes, the trigger waits for every download and can time out when many emails with attachments arrive at once. Microsoft’s fix: set it to No and fetch each file with the Get Attachment (V2) action instead.
  • If your company uses Safe Attachments with Dynamic Delivery, the trigger may run twice, the first time with no attachments. The loop in step 3 does nothing on the empty run, so this one costs you nothing.
  • Each connection allows 300 calls a minute to Outlook and 100 a minute to OneDrive.

Our view: this is the right default for a Microsoft 365 office. It runs without anyone’s PC switched on, costs nothing extra, and the folder fills as mail arrives. On the free license it can’t be shared, so it lives in one person’s account. Write down whose.

2. Sweep a whole folder with Power Automate for desktop

Power Automate for desktop drives the Outlook app on your PC, so it can work through a whole folder of old messages in one run.

It needs Windows 10 or 11, Home editions included, and doesn’t run on ARM processors. Its Outlook actions work only with classic Outlook, not the new one. Installing from the Microsoft Store needs no admin rights; the MSI installer does. We had no Windows PC in this run, so we did not test these steps.

  1. Create a desktop flow and add Launch Outlook.
  2. Add Retrieve email messages from Outlook. Set Account to your mailbox name, Mail folder to the folder (for a subfolder, the full path, such as Inbox\Invoices), and Retrieve to Unread email messages only. Leave Mark as read on.
  3. In the same action, set Attachments to Save attachments and Save attachments into to a folder that already exists. A missing folder stops the flow with “Directory for saving attachments not found”.
  4. Add Process email messages in Outlook with the operation Move email messages to mail folder, pointing at a folder such as Inbox\Saved. The next run then can’t pick up the same messages.
  5. Add Close Outlook, then run the flow.

Microsoft’s reference does not say how this action names files, whether it skips inline images, or what it does when two attachments share a name. Before you point it at a full folder, send yourself two emails that each carry a file called invoice.pdf, plus one with a signature logo, and look at what lands in the folder.

The other catch is the schedule. With a free personal or work account, the flow runs when you press Run. Triggering desktop flows from the cloud needs a premium organization account and Windows Pro or Enterprise. That makes this tool best for the backlog, and the cloud flow better for what arrives next week.

3. Run a script on messages saved as .eml files

This route works for the new Outlook, Outlook on the web and any computer with Python 3. It is the one we ran end to end.

Its weakness is the first step. Outlook on the web saves a message you select through More actions and then Download, as an EML or MSG file in your Downloads folder. The new Outlook uses More actions and then Save as; Microsoft’s page doesn’t name the file type, so check that the files end in .eml. The script reads only .eml files.

  1. Save the messages as .eml files into one folder, for example Downloads/mail.
  2. Save the 56-line script below as save_attachments.py.
  3. Run python3 save_attachments.py Downloads/mail Downloads/attachments.
"""Save the attachments from a folder of .eml files, skipping inline images.

Usage: python3 save_attachments.py <folder of .eml files> <output folder>
Names each file YYYY-MM-DD_sender-domain_original-name, never overwrites,
and skips exact duplicates, so it is safe to run again. Standard library only.
"""
import sys, re, hashlib, pathlib, email, email.policy
from email.utils import parsedate_to_datetime, parseaddr

BAD = r'[\\/:*?"<>|\x00-\x1f]'        # characters Windows will not accept in a file name

def clean(name):
    return re.sub(BAD, "_", name).strip(" .")[:150] or "attachment"

def is_inline(part):
    disp = part.get_content_disposition()  # 'inline', 'attachment' or None
    return disp != "attachment" and (disp == "inline" or part["Content-ID"] is not None)

def main(src, out):
    src, out = pathlib.Path(src), pathlib.Path(out)
    out.mkdir(parents=True, exist_ok=True)
    seen = {hashlib.sha256(p.read_bytes()).hexdigest() for p in out.iterdir() if p.is_file()}
    saved = inline = dupes = 0
    for eml in sorted(src.glob("*.eml")):
        msg = email.message_from_bytes(eml.read_bytes(), policy=email.policy.default)
        day = parsedate_to_datetime(msg["Date"]).strftime("%Y-%m-%d") if msg["Date"] else "undated"
        domain = parseaddr(msg["From"] or "")[1].rpartition("@")[2] or "unknown"
        for part in msg.walk():
            if part.is_multipart():
                continue                        # containers, including forwarded emails
            name = part.get_filename()
            if not name and part.get_content_maintype() == "text":
                continue                        # the message body
            if is_inline(part):
                inline += 1
                print(f"  skip inline  {eml.name}: {name or part['Content-ID']}")
                continue
            data = part.get_payload(decode=True) or b""
            digest = hashlib.sha256(data).hexdigest()
            if digest in seen:
                dupes += 1
                print(f"  skip dupe    {eml.name}: {name}")
                continue
            seen.add(digest)
            target = out / clean(f"{day}_{domain}_{name or 'attachment'}")
            n = 2
            while target.exists():
                target = target.with_name(f"{target.stem}_{n}{target.suffix}")
                n += 1
            target.write_bytes(data)
            saved += 1
            print(f"  saved        {target.name}")
    print(f"saved {saved}, inline skipped {inline}, duplicates skipped {dupes}")

if __name__ == "__main__":
    main(sys.argv[1], sys.argv[2])

What it printed on our test set

This is its real output on our 12 sample emails, run with Python 3.11 on Linux in under a tenth of a second (three runs took 0.080 to 0.091 seconds):

  skip inline  01.eml: image001.png
  saved        2026-10-01_vendor-a.com_invoice.pdf
  saved        2026-10-02_vendor-b.com_invoice.pdf
  saved        2026-10-03_vendor-c.com_invoice.pdf
  skip inline  04.eml: image001.png
  saved        2026-10-05_client.com_report.xlsx
  saved        2026-10-06_client.com_Q3_ _final_ report_.pdf
  saved        2026-10-07_leverantor.se_Faktura_åäö.pdf
  saved        2026-10-08_client.com_contract.pdf
  saved        2026-10-12_builder.com_photo.jpg
  skip inline  10.eml: <shot1>
  skip dupe    11.eml: invoice.pdf
  saved        2026-10-15_client.com_winmail.dat
saved 9, inline skipped 3, duplicates skipped 1

How it decides what to skip

Python’s get_content_disposition() returns inline, attachment or nothing. The script treats a part as an inline image when it is marked inline, or when it carries a Content-ID and is not marked as an attachment, which is how our pasted screenshot arrived. A forwarded email counts as a container in Python’s library, so walk() goes inside it and saves the contract it carries. Duplicates are caught by a fingerprint of the file’s content, so a resent invoice is skipped even under a new subject.

Two limits, both from our run. The script saves a winmail.dat file as it is and does not unpack what it holds. And we ran the script on Linux only. The name cleaning follows Microsoft’s list of reserved characters, but we did not run it on a Windows PC.

Check the folder after the first week

Whichever way you pick, a saving job that breaks looks exactly like a quiet week with no invoices. After seven days, count the files in the folder against the emails with attachments in the inbox for the same days. A gap or a broken file points to one of the documented limits: a message over 50 MB, a signed email, an attached email the cloud flow can’t read.

If you are still deciding whether attachments are the job worth automating first, a one-week tally will tell you. Our guide to scheduled scripts that fail silently has the checks that make a broken job speak up, and the weekly report guide uses the same free Power Automate license for its schedule. When the data you need sits in the email text rather than an attachment, our email-to-Excel guide tests three ways to pull it into a table. More guides like this one are under automation.

Then put the count in your calendar as a quarterly reminder today, before the folder fills up and nobody remembers what should be in it.

Sources