Skip to content

File attachments — attach a file to a task and read its bytes back.

A task attachment is a taskFileAttachment whose content travels as a base64-encoded contentBytes string. This script attaches a small file (generated in memory, or --file if given), lists the attachments, decodes the content into the temp directory, and removes it again.

Microsoft Graph only accepts simple (inline) attachments up to 3 MB for To Do; upload sessions for larger files are not currently supported by the service.

python file.py
python file.py --file report.pdf --task "Review Q3"

Permissions

Requires delegated permission Tasks.ReadWrite for the signed-in user.

Reference

View source

from __future__ import annotations

import argparse
import base64
import mimetypes
import pathlib
import tempfile

from office365.graph_client import GraphClient
from office365.runtime.client_request_exception import ClientRequestException
from office365.runtime.transfer import INLINE_ATTACHMENT_MAX
from tests.settings import client_id, password, tenant, username

LIST_NAME = "SDK Tasks Demo"
PERMISSION_CODES = {"accessDenied", "Authorization_RequestDenied", "ErrorAccessDenied", "Forbidden"}
_PREVIEW_BYTES = 120


def _client() -> GraphClient:
    return GraphClient(tenant=tenant).with_username_and_password(client_id, username, password)


def _source(args: argparse.Namespace) -> tuple[str, bytes, str]:
    if args.file:
        path = pathlib.Path(args.file)
        content_type = mimetypes.guess_type(path.name)[0] or "application/octet-stream"
        return path.name, path.read_bytes(), content_type
    name = "todo-note.txt"
    return name, b"Generated by the office365-rest-python-client To Do example.\n", "text/plain"


def main() -> None:
    parser = argparse.ArgumentParser(description="Attach a small file to a Microsoft To Do task")
    parser.add_argument("--file", help="path to the file to attach (default: an in-memory text note)")
    parser.add_argument("--task", default="Review the draft", help="task title")
    parser.add_argument("--keep", action="store_true", help="keep the list and task instead of deleting them")
    args = parser.parse_args()

    client = _client()
    try:
        name, content, content_type = _source(args)
        if len(content) > INLINE_ATTACHMENT_MAX:
            print(f"{name} is {len(content)} bytes; To Do only supports inline attachments up to 3 MB.")
            return

        task_list = client.me.todo.lists.ensure(LIST_NAME).execute_query()
        task = task_list.tasks.ensure(args.task).execute_query()

        attachment = task.upload_attachment(content, name, content_type)
        print(f"Attached {attachment.name!r} ({len(content)} bytes, {attachment.content_type})")

        print("Attachments on the task:")
        for item in task.attachments.get().execute_query():
            print(f"  {item.name:30s} {item.size:>8d} bytes  {item.content_type}  {item.last_modified_datetime}")

        stored = task.attachments[attachment.id].get().execute_query()
        raw = base64.b64decode(stored.content_bytes or "")
        out_dir = pathlib.Path(tempfile.gettempdir()) / "todo-attachments"
        out_dir.mkdir(exist_ok=True)
        out_path = out_dir / stored.name
        out_path.write_bytes(raw)
        print(f"\nRead back {len(raw)} bytes -> {out_path}")
        print(f"Preview: {raw[:_PREVIEW_BYTES]!r}")

        attachment.delete_object().execute_query()
        print("Removed the attachment.")

        if args.keep:
            print("Keeping the list and task (--keep).")
            return
        task.delete_object().execute_query()
        client.me.todo.lists[task_list.id].delete_object().execute_query()
        print("Cleaned up the demo list and task.")
    except ClientRequestException as exc:
        if exc.code not in PERMISSION_CODES:
            raise
        print(f"To Do request failed: {exc.code}: {exc.message}")
        print("Grant and consent the delegated permission Tasks.ReadWrite for the signed-in user, then re-run.")
        raise SystemExit(0) from None


if __name__ == "__main__":
    main()

← Back to Attachments and linked resources