Your AI Can’t Attach Files in Gmail. Two Ways to Fix It.

Last Saturday I asked my AI assistant to email three PDFs to two of my athletes. It wrote a better cover note than I would have. Correct recipients, right thread, sharp summary of each document.

It attached nothing. And it could not.

The files were three 25-page brand audits — Li-Ning, Way of Wade, and Serious Player Only — built with Cam Hazzard and Dylan Haugen to pitch shoe brands on something better than a sponsorship. About 24MB across the three. The assistant drafted the email in seconds and then hit a wall that most people never see, because most people never ask an AI to send a real file.

Here is what that wall actually is, and the two ways through it. Both are working on my machine right now. Pick one and you will never hand-attach a file for your assistant again.

Why your AI cannot attach files (three separate reasons)

People assume this is a permissions problem. It usually is not. There are three distinct failures and they need different fixes.

1. The email connector often cannot send at all

Most AI Gmail integrations expose a create draft function and nothing else. No send. That is a deliberate safety choice and a reasonable one — you want a human looking at outbound mail in your name. But it means “email this to Bob” was never going to fully work, no matter how you phrased it.

2. Attachments have to travel through the model’s context

This is the real blocker. Connector APIs take attachment content as base64 text inside the function call. Base64 inflates a file by about a third. My 24MB of PDFs becomes roughly 32MB of text that the model has to literally write out, character by character, inside a single request.

No model has that kind of room. This is not a limit that gets better with a bigger context window either — you are asking a language model to retype a binary file. It is the wrong tool for moving bytes.

3. Browser upload is gated to files you shared

The obvious workaround is to drive the browser: open Gmail, click attach, upload. Some AI browser tools do have a file-upload function, and Gmail’s file input is easy to find.

It still fails. Those tools only permit uploading files you handed to the session. A file the assistant generated on your disk gets rejected. Sensible security design, and a hard stop for this use case.

So: no send, no inline attachments, no browser upload. Every obvious road is closed. Both fixes below work by getting out of the chat window entirely.

Method 1: Give your assistant the Gmail API

This is the one that actually solves the problem. You create a small script that talks to Gmail directly. The assistant runs the script; the file never touches the model’s context. Attachments become a non-issue up to Gmail’s real 25MB ceiling.

Budget five minutes. You do it once.

Step 1 — Create a Google Cloud project

Go to console.cloud.google.com/projectcreate. Name it something you will recognize later. Create it, then confirm it is selected in the project picker at the top — this trips people up constantly.

Step 2 — Enable the Gmail API

Visit the Gmail API page and click Enable. That is the whole step.

Step 3 — Configure the consent screen and add yourself as a test user

Under APIs & Services, open the Auth/consent section. Choose External, put your own email in the support and developer fields, and save.

Then find Test users and add your own Google account. Do not skip this. It is the single most common failure and I will come back to it below.

Step 4 — Create a Desktop OAuth client

Create credentials, choose Desktop app as the application type, and download the JSON. Save it next to your script as client_secret.json.

Step 5 — Install the libraries in a virtual environment

On a modern Mac, do not install into system Python. Homebrew’s Python refuses package installs by design, and forcing it breaks things you will miss later.

mkdir -p ~/tools/gmail && cd ~/tools/gmail
python3 -m venv venv
./venv/bin/pip install google-api-python-client google-auth-oauthlib google-auth-httplib2

Step 6 — Authorize once

Your script’s first run opens a browser. Sign in, approve, done. The token gets cached so this never repeats.

The core of the script is short. Build a standard email message, attach files by reading them off disk, base64-encode the whole message, and hand it to Gmail:

msg = EmailMessage()
msg["To"] = "someone@example.com"
msg["Subject"] = "Here are the files"
msg.set_content(body_text)

for path in attachments:
    data = pathlib.Path(path).read_bytes()
    ctype, _ = mimetypes.guess_type(path)
    maintype, _, subtype = (ctype or "application/octet-stream").partition("/")
    msg.add_attachment(data, maintype=maintype, subtype=subtype,
                       filename=pathlib.Path(path).name)

raw = base64.urlsafe_b64encode(msg.as_bytes()).decode()
service.users().drafts().create(userId="me", body={"message": {"raw": raw}}).execute()

Swap drafts().create() for messages().send() when you want it to go straight out. I keep mine on drafts. I want to read anything that leaves under my name.

Two details worth adding. Pass a threadId so replies stay threaded instead of starting a new conversation. And check the total attachment size before you call the API, so you get a clear error instead of a confusing rejection.

My version now produces a finished draft — recipients, threading, body, and three PDFs totaling 19MB — from a single command. I read it and hit send.

Method 2: Google Drive for Desktop

The second fix does not send email at all. It gives your assistant a folder that is Drive, so files can be staged and shared as links.

Install Google Drive for Desktop, sign in, allow the system extension, and keep the default “Stream files” mode. You get a path like ~/Library/CloudStorage/GoogleDrive-you@gmail.com/ with My Drive and every Shared Drive underneath it.

Now anything written to that path is in Drive. I tested it: a file copied into our Public Assets folder appeared in Drive with a real file ID and shareable URL within seconds. No upload code, no API call — just a file copy.

This is the answer for anything over 25MB, and for video, which blows past email limits immediately.

One warning. The setup wizard offers to sync your Desktop, Documents, and Downloads folders into Drive, with checkboxes pre-ticked. On my machine that was 585GB of Desktop and 168GB of Downloads. Look at those numbers before you click Next. Streaming Drive down to your Mac is what you want; pushing three-quarters of a terabyte up is probably not.

Which one should you use

  Gmail API Drive for Desktop
Setup time ~5 minutes, once ~5 minutes, once
Sends actual email Yes No
Real attachments Yes, up to 25MB No — links instead
Large files and video No Yes, no practical limit
Recipient experience File is right there Click a link, maybe request access
You can revoke it One click in account settings Quit the app
Best for Proposals, audits, invoices, decks Video, raw footage, big archives

Set up both. They solve different problems and neither takes long. The Gmail API handles the daily case — a document going to a client. Drive handles the case where email was never going to work anyway.

The five errors that cost me time

Every one of these hit me. None are in the official docs in a form you would find while stuck.

“Access blocked … has not completed the Google verification process” — Error 403

This means you are not on the test user list. It is a hard block with no way past it in the interface.

Do not confuse it with the milder “Google hasn’t verified this app” screen, which does have Advanced → Go to (unsafe). If you are looking for that link and cannot find it, you have the first error, and the fix is adding your email under Test users.

Homebrew Python refuses to install packages

You will see an externally-managed-environment error. There is a flag that overrides it. Do not use it. Create a virtual environment instead — it takes one command and cannot break your system Python.

Your download glob matches two files

mv ~/Downloads/client_secret_*.json ... fails with “is not a directory” if you have downloaded credentials before. The glob matched two files, and mv then wants the last argument to be a folder. Check what is actually in Downloads and move the one you just created.

Filenames with em-dashes and ampersands

Our audits were named with em-dashes and an ampersand, which is fine on disk and a problem in tooling. Rename to plain ASCII with hyphens before attaching. It also looks more professional in someone’s inbox.

The console moves

Google reshuffles the OAuth screens regularly. Pin the project in the URL with ?project=your-project-id so you are never editing settings in the wrong project and wondering why nothing changed.

Keep it locked down

You are handing an assistant the ability to email as you. Be deliberate.

  • Scope it narrowly. Use gmail.modify. That covers drafts and sending. It does not touch Drive, contacts, or calendar.
  • Default to drafts. Sending is one word away when you want it. Making review the default costs you a click and saves you an incident.
  • Protect the credential files. chmod 600 on the client secret and token, in a folder set to 700. Add both to .gitignore before they exist, not after.
  • Never let secrets into a chat window. The script reads them off disk. They should never be printed, pasted, or summarized into a prompt.
  • Know how to revoke. myaccount.google.com/permissions, remove the app. Deleting the token file forces re-authorization.

Why this is worth five minutes

The gap between “my AI wrote the email” and “my AI sent the email with the file” looks trivial. It is not. It is the difference between an assistant that drafts and an assistant that finishes.

Before this, every document my team produced ended the same way: a note telling me which files to attach. Multiply that by the audits, proposals, and reports going out every week and you are doing mechanical work that the machine was ninety percent of the way through.

That is the same principle behind Dollar a Day and the Content Factory — do the setup once, correctly, and stop paying the tax forever. A five-minute OAuth flow removes a step you were going to repeat several hundred times this year.

Set up both methods. Then ask your assistant to email something with a file attached, and watch it actually finish the job.

Questions about wiring this into your own stack? Reach out — we do this with clients every week.

Dennis Yu
Dennis Yu
Dennis Yu is the CEO of Local Service Spotlight, a platform that amplifies the reputations of contractors and local service businesses using the Content Factory process. He is a former search engine engineer who has spent a billion dollars on Google and Facebook ads for Nike, Quiznos, Ashley Furniture, Red Bull, State Farm, and other brands. Dennis has achieved 25% of his goal of creating a million digital marketing jobs by partnering with universities, professional organizations, and agencies. Through Local Service Spotlight, he teaches the Dollar a Day strategy and Content Factory training to help local service businesses enhance their existing local reputation and make the phone ring. Dennis coaches young adult agency owners serving plumbers, AC technicians, landscapers, roofers, electricians, and believes there should be a standard in measuring local marketing efforts, much like doctors and plumbers must be certified. He has appeared on 353 podcasts with 619 credited episodes — see the full list of his podcast appearances.