File size: 3,896 Bytes
b4f2238
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
"""收件箱 API 端点。"""

from __future__ import annotations

import logging
from typing import Any

from django.http import FileResponse, HttpRequest
from ninja import Query, Router

from apps.core.exceptions import NotFoundError
from apps.message_hub.models import InboxMessage
from apps.message_hub.schemas import InboxMessageDetailOut, InboxMessageOut

logger = logging.getLogger("apps.message_hub")
router = Router()

# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------


def _get_base_queryset() -> Any:
    return InboxMessage.objects.select_related("source", "source__credential").order_by("-received_at")


def _get_message_or_404(pk: int) -> InboxMessage:
    try:
        return _get_base_queryset().get(pk=pk)
    except InboxMessage.DoesNotExist:
        raise NotFoundError(f"消息 {pk} 不存在")


# ---------------------------------------------------------------------------
# Endpoints
# ---------------------------------------------------------------------------


@router.get("/messages", response=list[InboxMessageOut])
def list_messages(
    request: HttpRequest,
    source_id: int | None = None,
    has_attachments: bool | None = None,
    search: str | None = None,
) -> Any:
    """收件箱消息列表。"""
    qs = _get_base_queryset()

    if source_id is not None:
        qs = qs.filter(source_id=source_id)
    if has_attachments is not None:
        qs = qs.filter(has_attachments=has_attachments)
    if search:
        from django.db.models import Q

        qs = qs.filter(Q(subject__icontains=search) | Q(sender__icontains=search) | Q(body_text__icontains=search))

    return qs


@router.get("/messages/{message_id}", response=InboxMessageDetailOut)
def get_message(request: HttpRequest, message_id: int) -> Any:
    """收件箱消息详情。"""
    return _get_message_or_404(message_id)


@router.get("/messages/{message_id}/attachments/{part_index}/download")
def download_attachment(
    request: HttpRequest,
    message_id: int,
    part_index: int,
) -> FileResponse:
    """下载附件。"""
    msg = _get_message_or_404(message_id)
    return _serve_attachment(msg, part_index, inline=False)


@router.get("/messages/{message_id}/attachments/{part_index}/preview")
def preview_attachment(
    request: HttpRequest,
    message_id: int,
    part_index: int,
) -> FileResponse:
    """预览附件(inline)。"""
    msg = _get_message_or_404(message_id)
    return _serve_attachment(msg, part_index, inline=True)


def _resolve_download_filename(msg: InboxMessage, part_index: int, fallback: str) -> str:
    for att in msg.attachments_meta or []:
        if int(att.get("part_index", -1)) != part_index:
            continue
        custom_name = str(att.get("custom_filename", "")).strip()
        if custom_name:
            return custom_name
        original_name = str(att.get("original_filename") or att.get("filename") or "").strip()
        if original_name:
            return original_name
    return fallback


def _serve_attachment(msg: InboxMessage, part_index: int, *, inline: bool) -> FileResponse:
    """通过 fetcher 按需下载并返回附件。"""
    from apps.message_hub.services import get_fetcher

    fetcher = get_fetcher(msg.source.source_type)
    content, filename, content_type = fetcher.download_attachment(
        msg.source,
        msg.message_id,
        part_index,
    )
    download_filename = _resolve_download_filename(msg, part_index, filename)
    disposition = "inline" if inline else "attachment"
    response = FileResponse(
        iter([content]),
        content_type=content_type,
        as_attachment=not inline,
        filename=download_filename,
    )
    response["Content-Disposition"] = f'{disposition}; filename="{download_filename}"'
    return response