Skip to main content
Glama

create_email_draft

Create email drafts in native inbox (Outlook/Gmail). Start new emails or reply to existing threads, attach local files, and format body in Markdown.

Instructions

Creates an email draft in the user's native draft box (e.g., Outlook/Gmail). Can either start a NEW email, or REPLY to an existing thread. To REPLY, provide 'reply_to_email_id' (the short ID from search_and_fetch_emails). To start a NEW email, omit the ID but provide 'subject' and 'to_recipients'. Allows attaching local files (PDF/DOCX) by providing their absolute paths. The body should be formatted in Markdown.

Input Schema

TableJSON Schema
NameRequiredDescriptionDefault
body_markdownYesThe body of the email in Markdown format. Will be converted to HTML.
reply_to_email_idNoProvide the short email ID to reply to an existing thread.
subjectNoThe subject line. Required if starting a NEW email.
to_recipientsNoList of emails. Required if starting a NEW email.
attachment_pathsNoList of absolute file paths on the local system to attach to the draft.

Implementation Reference

  • The actual implementation of the create_email_draft tool. It validates inputs (reply_to_email_id or subject+to_recipients), resolves email IDs, parses recipients/attachments, reads attachment bytes, encodes a multipart form-data request, and POSTs to the Adeu Cloud backend to create an email draft.
    async def create_email_draft(
        ctx: Context,
        body_markdown: Annotated[str, "The body of the email in Markdown format. Will be converted to HTML."],
        reply_to_email_id: Annotated[Optional[str], "Provide the short email ID to reply to an existing thread."] = None,
        subject: Annotated[Optional[str], "The subject line. Required if starting a NEW email."] = None,
        to_recipients: Annotated[Optional[list[str] | str], "List of emails. Required if starting a NEW email."] = None,
        attachment_paths: Annotated[
            Optional[list[str] | str],
            "List of absolute file paths on the local system to attach to the draft.",
        ] = None,
        api_key: str = Depends(get_cloud_auth_token),
    ) -> ToolResult:
        # 1. Validation
        if not reply_to_email_id and (not subject or not to_recipients):
            return ToolResult(
                "Error: You must provide either 'reply_to_email_id' (to reply) OR "
                "both 'subject' and 'to_recipients' (to start a new email).",
            )
    
        await ctx.info(
            "Creating email draft",
            extra={"reply_to": reply_to_email_id, "subject": subject},
        )
        url = f"{BACKEND_URL}/api/v1/emails/drafts/new"
    
        # Helper to safely parse stringified lists from Claude
        def _parse_list(val) -> list[str]:
            if not val:
                return []
            if isinstance(val, list):
                return val
            try:
                parsed = json.loads(val)
                return parsed if isinstance(parsed, list) else [val]
            except Exception:
                return [x.strip() for x in val.split(",") if x.strip()]
    
        parsed_recipients = _parse_list(to_recipients)
        parsed_attachments = _parse_list(attachment_paths)
    
        # 2. Resolve Minified ID to Real Graph ID
        real_reply_to_id = resolve_email_id(reply_to_email_id) if reply_to_email_id else None
    
        # Prepare text fields
        fields = {
            "body_markdown": body_markdown,
        }
        if real_reply_to_id:
            fields["reply_to_email_id"] = real_reply_to_id
        if subject:
            fields["subject"] = subject
        if parsed_recipients:
            fields["to_recipients"] = json.dumps(parsed_recipients)
    
        # Prepare file bytes
        files_to_upload = []
        if parsed_attachments:
            for path in parsed_attachments:
                try:
                    file_bytes = read_file_bytes(path).getvalue()
                    filename = Path(path).name
                    files_to_upload.append(("files", filename, file_bytes))
                except Exception as e:
                    raise ToolError(f"Failed to read attachment {path}: {e}") from e
    
        # Encode payload
        body, content_type = encode_multipart_formdata(fields=fields, files=files_to_upload)
    
        req = urllib.request.Request(
            url,
            data=body,
            headers={
                "Authorization": f"Bearer {api_key}",
                "Content-Type": content_type,
                "Accept": "application/json",
            },
            method="POST",
        )
    
        try:
            await ctx.debug("Sending draft request to Adeu Cloud")
            with urllib.request.urlopen(req) as response:
                data = json.loads(response.read().decode("utf-8"))
                draft_id = data.get("id")
                return ToolResult(content=f"Successfully created email draft! Draft ID: {draft_id}")
        except urllib.error.HTTPError as e:
            if e.code == 401:
                DesktopAuthManager.clear_api_key()
                raise ToolError("Authentication expired. Please call `login_to_adeu_cloud` to re-authenticate.") from e
            error_body = e.read().decode("utf-8")
            raise ToolError(f"Cloud draft creation failed (HTTP {e.code}): {error_body}") from e
        except Exception as e:
            raise ToolError(f"Failed to communicate with Adeu Cloud: {str(e)}") from e
  • The function signature uses Annotated type hints to define the input parameters: body_markdown (str), reply_to_email_id (Optional[str]), subject (Optional[str]), to_recipients (Optional[list[str]|str]), attachment_paths (Optional[list[str]|str]), and api_key (injected via Depends).
    async def create_email_draft(
        ctx: Context,
        body_markdown: Annotated[str, "The body of the email in Markdown format. Will be converted to HTML."],
        reply_to_email_id: Annotated[Optional[str], "Provide the short email ID to reply to an existing thread."] = None,
        subject: Annotated[Optional[str], "The subject line. Required if starting a NEW email."] = None,
        to_recipients: Annotated[Optional[list[str] | str], "List of emails. Required if starting a NEW email."] = None,
        attachment_paths: Annotated[
            Optional[list[str] | str],
            "List of absolute file paths on the local system to attach to the draft.",
        ] = None,
        api_key: str = Depends(get_cloud_auth_token),
  • The @tool decorator registers 'create_email_draft' with its name and description, explaining how to start a new email vs reply, and that attachments and Markdown body are supported.
    @tool(
        name="create_email_draft",
        description=(
            "Creates an email draft in the user's native draft box (e.g., Outlook/Gmail). "
            "Can either start a NEW email, or REPLY to an existing thread. "
            "To REPLY, provide 'reply_to_email_id' (the short ID from search_and_fetch_emails). "
            "To start a NEW email, omit the ID but provide 'subject' and 'to_recipients'. "
            "Allows attaching local files (PDF/DOCX) by providing their absolute paths. "
            "The body should be formatted in Markdown."
        ),
  • encode_multipart_formdata helper used to encode the email draft payload (fields + file attachments) into multipart/form-data format for the HTTP POST request.
    def encode_multipart_formdata(
        fields: Optional[Dict[str, str]] = None,
        files: Optional[List[tuple[str, str, bytes]]] = None,
    ) -> tuple[bytes, str]:
        boundary = uuid.uuid4().hex
        buffer = BytesIO()
    
        if fields:
            for key, value in fields.items():
                buffer.write(f"--{boundary}\r\n".encode("utf-8"))
                buffer.write(f'Content-Disposition: form-data; name="{key}"\r\n\r\n'.encode("utf-8"))
                buffer.write(value.encode("utf-8"))
                buffer.write(b"\r\n")
    
        if files:
            for field_name, file_name, file_bytes in files:
                buffer.write(f"--{boundary}\r\n".encode("utf-8"))
                buffer.write(
                    f'Content-Disposition: form-data; name="{field_name}"; filename="{file_name}"\r\n'.encode("utf-8")
                )
                content_type = mimetypes.guess_type(file_name)[0] or "application/octet-stream"
                buffer.write(f"Content-Type: {content_type}\r\n\r\n".encode("utf-8"))
                buffer.write(file_bytes)
                buffer.write(b"\r\n")
    
        buffer.write(f"--{boundary}--\r\n".encode("utf-8"))
        return buffer.getvalue(), f"multipart/form-data; boundary={boundary}"
  • read_file_bytes helper used to read attachment files from the local filesystem into BytesIO objects.
    def read_file_bytes(path: str) -> BytesIO:
        p = Path(path)
        if not p.exists():
            raise FileNotFoundError(f"File not found: {path}")
        with open(p, "rb") as f:
            return BytesIO(f.read())
Behavior4/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

With no annotations, the description carries full burden. It discloses that drafts are created in the native draft box (not sent), supports Markdown body, and accepts attachments (PDF/DOCX) via absolute paths. It does not mention permissions, limits, or what happens on failure. This is adequate but could add more safety context. Score 4.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness5/5

Is the description appropriately sized, front-loaded, and free of redundancy?

The description is a single, well-structured paragraph that first states the main function, then explains two modes, then attachments, then body format. Every sentence is informative; no redundant or vague statements. It is appropriately sized for the complexity. Score 5.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness3/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

The tool has 5 parameters and no output schema. The description explains input semantics well but omits what the tool returns (e.g., draft ID or success status). Given the complexity and that sibling tools like search_and_fetch_emails have IDs, the return value is important for chaining. Completeness is slightly lacking, so 3.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters5/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Schema coverage is 100%, baseline 3. The description adds significant value by explaining the relationship between parameters and the two modes (NEW vs REPLY). It clarifies that reply_to_email_id is required for REPLY, and subject/to_recipients are required for NEW. This goes beyond individual parameter descriptions and provides usage logic. Score 5.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

The description clearly states it creates an email draft in the user's native draft box (Outlook/Gmail). It distinguishes between starting a NEW email and REPLYING to a thread, and references the sibling tool search_and_fetch_emails for the reply ID. This specificity and differentiation merits a 5.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines4/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

The description explicitly provides two modes (REPLY vs NEW) with conditions for each: for REPLY, provide reply_to_email_id; for NEW, provide subject and to_recipients. It also instructs on attachment paths. However, it does not state when NOT to use this tool nor list alternatives, missing full comparatives. Still, the guidance is clear and useful, so 4.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

Install Server

Other Tools

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/dealfluence/adeu'

If you have feedback or need assistance with the MCP directory API, please join our Discord server