Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 18 additions & 1 deletion tools/gmail/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ A comprehensive Gmail integration plugin for Dify that provides essential mail-r
- **Send Drafts**: Send previously created draft emails

### **Attachment Support**
- **Download Attachments**: Download attachment content from emails
- **Add Attachments**: Attach files to existing draft emails

### **Email Organization**
Expand Down Expand Up @@ -100,14 +101,29 @@ parameters:
action: "flag"
```

### Download an Attachment
```yaml
# First, get message details with attachments
tool: get_message
parameters:
message_id: "18c1a2b3d4e5f6g7"
include_attachments: true

# Then, download the attachment using the attachment_id from above
tool: download_attachment
parameters:
message_id: "18c1a2b3d4e5f6g7"
attachment_id: "ANGjdJ8wXYZ..."
```

## Tool Reference

### Core Tools

| Tool | Description | Key Parameters |
|------|-------------|----------------|
| `search_messages` | Advanced Gmail search | `query`, `max_results`, `include_body` |
| `get_message` | Get detailed message information | `message_id`, `include_body` |
| `get_message` | Get detailed message information | `message_id`, `include_body`, `include_attachments` |

### Email Composition

Expand All @@ -122,6 +138,7 @@ parameters:

| Tool | Description | Key Parameters |
|------|-------------|----------------|
| `download_attachment` | Download attachment content | `message_id`, `attachment_id` |
| `add_attachment_to_draft` | Add file to draft | `draft_id`, `file_path` |
| `flag_message` | Flag/unflag for follow-up | `message_id`, `action` |

Expand Down
2 changes: 1 addition & 1 deletion tools/gmail/manifest.yaml
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
version: 0.2.1
version: 0.2.2
type: plugin
author: langgenius
name: dify-gmail
Expand Down
129 changes: 129 additions & 0 deletions tools/gmail/tools/download_attachment.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
import base64
from collections.abc import Generator
from typing import Any

import requests
from dify_plugin import Tool
from dify_plugin.entities.tool import ToolInvokeMessage


class DownloadAttachmentTool(Tool):
def _invoke(self, tool_parameters: dict[str, Any]) -> Generator[ToolInvokeMessage]:
"""
Download an attachment from a Gmail message.

Returns the attachment content as base64-encoded data along with metadata.
To use this tool, you first need to call get_message with include_attachments=True
to get the message_id and attachment_id.
"""
try:
# Get parameters
message_id = tool_parameters.get("message_id", "").strip()
attachment_id = tool_parameters.get("attachment_id", "").strip()

if not message_id:
yield self.create_text_message("Error: Message ID is required.")
return

if not attachment_id:
yield self.create_text_message("Error: Attachment ID is required.")
return

# Get credentials from tool provider
access_token = self.runtime.credentials.get("access_token")

if not access_token:
yield self.create_text_message("Error: No access token available. Please authorize the Gmail integration.")
return

headers = {
"Authorization": f"Bearer {access_token}",
"Accept": "application/json"
}

# Download attachment
attachment_url = f"https://gmail.googleapis.com/gmail/v1/users/me/messages/{message_id}/attachments/{attachment_id}"

yield self.create_text_message(f"Downloading attachment from message {message_id}...")

attachment_response = requests.get(attachment_url, headers=headers, timeout=60)

if attachment_response.status_code == 401:
yield self.create_text_message("Error: Access token expired. Please re-authorize the Gmail integration.")
return
elif attachment_response.status_code == 404:
yield self.create_text_message("Error: Attachment not found. The message ID or attachment ID may be invalid.")
return
elif attachment_response.status_code != 200:
yield self.create_text_message(f"Error: Gmail API returned status {attachment_response.status_code}")
return

attachment_data = attachment_response.json()

# Gmail returns attachments with base64url-encoded data
encoded_data = attachment_data.get("data")
size_bytes = attachment_data.get("size", 0)

if not encoded_data:
yield self.create_text_message("Error: No attachment data received from Gmail API.")
return

# Decode the attachment content
try:
decoded_bytes = self._decode_base64url(encoded_data)
except ValueError as e:
yield self.create_text_message(f"Error decoding attachment data: {e}")
return

# Check size limit (25MB is Gmail's practical attachment limit)
MAX_ATTACHMENT_SIZE_BYTES = 25 * 1024 * 1024
if size_bytes > MAX_ATTACHMENT_SIZE_BYTES:
yield self.create_text_message(
f"Warning: Attachment size ({size_bytes} bytes) exceeds 25MB. "
"This may cause issues with some operations."
)

# Re-encode as standard base64 for output
content_base64 = base64.b64encode(decoded_bytes).decode("utf-8")

# Create output
yield self.create_text_message(
f"Successfully downloaded attachment ({size_bytes} bytes)."
)

yield self.create_variable_message("attachment_data", content_base64)
yield self.create_variable_message("attachment_size", str(size_bytes))

yield self.create_json_message({
"status": "success",
"message_id": message_id,
"attachment_id": attachment_id,
"size": size_bytes,
"data": content_base64,
"encoding": "base64"
})

except requests.RequestException as e:
yield self.create_text_message(f"Network error: {str(e)}")
except Exception as e:
yield self.create_text_message(f"Error downloading attachment: {str(e)}")

def _decode_base64url(self, data: str) -> bytes:
"""
Decode base64url encoded string to bytes.

Gmail uses base64url encoding (RFC 4648) where:
- '+' is replaced with '-'
- '/' is replaced with '_'
- Padding '=' may be omitted
"""
try:
# Use Python's built-in base64url decoder
# Add padding if needed
missing_padding = len(data) % 4
if missing_padding:
data += "=" * (4 - missing_padding)

return base64.urlsafe_b64decode(data)
except (binascii.Error, TypeError) as e:
raise ValueError(f"Failed to decode base64url data: {e}") from e
71 changes: 71 additions & 0 deletions tools/gmail/tools/download_attachment.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
identity:
name: "download_attachment"
display_name: "Download Attachment"
author: "langgenius"
label:
en_US: "Download Gmail Attachment"
zh_Hans: "下载Gmail附件"
pt_BR: "Baixar Anexo do Gmail"
ja_JP: "Gmail添付ファイルをダウンロード"
description:
human:
en_US: "Download an attachment from a Gmail message by message ID and attachment ID"
zh_Hans: "通过邮件ID和附件ID从Gmail邮件下载附件"
pt_BR: "Baixar um anexo de uma mensagem do Gmail por ID da mensagem e ID do anexo"
ja_JP: "メッセージIDと添付ファイルIDでGmailメッセージから添付ファイルをダウンロード"
llm: "Download the content of a specific attachment from a Gmail message. Returns base64-encoded attachment data along with metadata. Requires both message_id and attachment_id which can be obtained from the get_message tool."
parameters:
- name: message_id
type: string
required: true
label:
en_US: "Message ID"
zh_Hans: "邮件ID"
pt_BR: "ID da Mensagem"
ja_JP: "メッセージID"
human_description:
en_US: "The unique identifier of the Gmail message containing the attachment"
zh_Hans: "包含附件的Gmail邮件的唯一标识符"
pt_BR: "Identificador único da mensagem do Gmail contendo o anexo"
ja_JP: "添付ファイルを含むGmailメッセージの一意の識別子"
llm_description: "The unique message ID of the Gmail message. This can be obtained from get_message or search_messages tools."
form: llm
- name: attachment_id
type: string
required: true
label:
en_US: "Attachment ID"
zh_Hans: "附件ID"
pt_BR: "ID do Anexo"
ja_JP: "添付ファイルID"
human_description:
en_US: "The unique identifier of the attachment to download"
zh_Hans: "要下载的附件的唯一标识符"
pt_BR: "Identificador único do anexo a ser baixado"
ja_JP: "ダウンロードする添付ファイルの一意の識別子"
llm_description: "The unique attachment ID obtained from get_message tool with include_attachments=True. This ID is found in the 'attachment_id' field of the attachment info."
form: llm
output_schema:
type: object
properties:
status:
type: string
description: "Status of the download operation"
message_id:
type: string
description: "The message ID from which the attachment was downloaded"
attachment_id:
type: string
description: "The attachment ID that was downloaded"
size:
type: number
description: "Size of the attachment in bytes"
data:
type: string
description: "Base64-encoded attachment content"
encoding:
type: string
description: "Encoding format of the data (base64)"
extra:
python:
source: tools/download_attachment.py