fix: enhance email threading and extraction logic in sag email handling
This commit is contained in:
parent
b0a51f1919
commit
81cc3a4a9e
@ -1893,11 +1893,25 @@ async def add_sag_email_link(sag_id: int, payload: dict):
|
||||
async def get_sag_emails(sag_id: int):
|
||||
"""Get emails linked to a case."""
|
||||
query = """
|
||||
SELECT e.*
|
||||
FROM email_messages e
|
||||
JOIN sag_emails se ON e.id = se.email_id
|
||||
WHERE se.sag_id = %s
|
||||
ORDER BY e.received_date DESC
|
||||
WITH linked_emails AS (
|
||||
SELECT
|
||||
e.*,
|
||||
COALESCE(
|
||||
NULLIF(REGEXP_REPLACE(TRIM(COALESCE(e.in_reply_to, '')), '[<>\\s]', '', 'g'), ''),
|
||||
NULLIF(REGEXP_REPLACE((REGEXP_SPLIT_TO_ARRAY(COALESCE(e.email_references, ''), E'[\\s,]+'))[1], '[<>\\s]', '', 'g'), ''),
|
||||
NULLIF(REGEXP_REPLACE(TRIM(COALESCE(e.message_id, '')), '[<>\\s]', '', 'g'), ''),
|
||||
CONCAT('email-', e.id::text)
|
||||
) AS thread_key
|
||||
FROM email_messages e
|
||||
JOIN sag_emails se ON e.id = se.email_id
|
||||
WHERE se.sag_id = %s
|
||||
)
|
||||
SELECT
|
||||
linked_emails.*,
|
||||
COUNT(*) OVER (PARTITION BY linked_emails.thread_key) AS thread_message_count,
|
||||
MAX(linked_emails.received_date) OVER (PARTITION BY linked_emails.thread_key) AS thread_last_received_date
|
||||
FROM linked_emails
|
||||
ORDER BY thread_last_received_date DESC NULLS LAST, received_date DESC
|
||||
"""
|
||||
return execute_query(query, (sag_id,)) or []
|
||||
|
||||
@ -1969,6 +1983,8 @@ async def upload_sag_email(sag_id: int, file: UploadFile = File(...)):
|
||||
|
||||
email_data = {
|
||||
'message_id': msg.get('Message-ID', f"eml-{temp_id}"),
|
||||
'in_reply_to': _decode_header_str(msg.get('In-Reply-To', '')),
|
||||
'email_references': _decode_header_str(msg.get('References', '')),
|
||||
'subject': _decode_header_str(msg.get('Subject', 'No Subject')),
|
||||
'sender_email': _decode_header_str(msg.get('From', '')),
|
||||
'sender_name': _decode_header_str(msg.get('From', '')),
|
||||
|
||||
@ -5837,20 +5837,40 @@
|
||||
return;
|
||||
}
|
||||
setModuleContentState('emails', true);
|
||||
container.innerHTML = emails.map(e => `
|
||||
<div class="list-group-item">
|
||||
<div class="d-flex justify-content-between align-items-start">
|
||||
<div class="text-truncate">
|
||||
<i class="bi bi-envelope text-primary me-1"></i>
|
||||
<strong>${e.subject || '(Ingen emne)'}</strong>
|
||||
<div class="small text-muted text-truncate">${e.sender_email}</div>
|
||||
</div>
|
||||
<button class="btn btn-sm btn-link text-danger p-0 ms-2" onclick="unlinkEmail(${e.id})">
|
||||
<i class="bi bi-link-45deg" style="text-decoration: line-through;"></i>
|
||||
</button>
|
||||
const threadMap = new Map();
|
||||
emails.forEach(e => {
|
||||
const key = e.thread_key || `email-${e.id}`;
|
||||
if(!threadMap.has(key)) threadMap.set(key, []);
|
||||
threadMap.get(key).push(e);
|
||||
});
|
||||
|
||||
const threads = Array.from(threadMap.values());
|
||||
container.innerHTML = threads.map((threadEmails, threadIndex) => {
|
||||
const labelEmail = threadEmails[0];
|
||||
const messageCount = labelEmail.thread_message_count || threadEmails.length;
|
||||
return `
|
||||
<div class="list-group-item p-0">
|
||||
<div class="px-3 py-2 border-bottom bg-light d-flex justify-content-between align-items-center">
|
||||
<span class="small fw-semibold text-secondary">Tråd ${threadIndex + 1}</span>
|
||||
<span class="badge bg-primary-subtle text-primary-emphasis">${messageCount} beskeder</span>
|
||||
</div>
|
||||
${threadEmails.map(e => `
|
||||
<div class="px-3 py-2 border-bottom">
|
||||
<div class="d-flex justify-content-between align-items-start">
|
||||
<div class="text-truncate">
|
||||
<i class="bi bi-envelope text-primary me-1"></i>
|
||||
<strong>${e.subject || '(Ingen emne)'}</strong>
|
||||
<div class="small text-muted text-truncate">${e.sender_email || '-'}</div>
|
||||
</div>
|
||||
<button class="btn btn-sm btn-link text-danger p-0 ms-2" onclick="unlinkEmail(${e.id})">
|
||||
<i class="bi bi-link-45deg" style="text-decoration: line-through;"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
`).join('')}
|
||||
</div>
|
||||
`).join('');
|
||||
`;
|
||||
}).join('');
|
||||
}
|
||||
|
||||
async function unlinkEmail(emailId) {
|
||||
|
||||
@ -221,6 +221,49 @@ class EmailWorkflowService:
|
||||
return int(match.group(1))
|
||||
return None
|
||||
|
||||
def _normalize_message_id(self, value: Optional[str]) -> Optional[str]:
|
||||
if not value:
|
||||
return None
|
||||
normalized = re.sub(r'[<>\s]', '', str(value)).lower().strip()
|
||||
return normalized or None
|
||||
|
||||
def _extract_thread_message_ids(self, email_data: Dict) -> List[str]:
|
||||
tokens: List[str] = []
|
||||
|
||||
in_reply_to = self._normalize_message_id(email_data.get('in_reply_to'))
|
||||
if in_reply_to:
|
||||
tokens.append(in_reply_to)
|
||||
|
||||
raw_references = (email_data.get('email_references') or '').strip()
|
||||
if raw_references:
|
||||
for ref in re.split(r'[\s,]+', raw_references):
|
||||
normalized_ref = self._normalize_message_id(ref)
|
||||
if normalized_ref:
|
||||
tokens.append(normalized_ref)
|
||||
|
||||
# De-duplicate while preserving order
|
||||
return list(dict.fromkeys(tokens))
|
||||
|
||||
def _find_sag_id_from_thread_headers(self, email_data: Dict) -> Optional[int]:
|
||||
thread_message_ids = self._extract_thread_message_ids(email_data)
|
||||
if not thread_message_ids:
|
||||
return None
|
||||
|
||||
placeholders = ','.join(['%s'] * len(thread_message_ids))
|
||||
rows = execute_query(
|
||||
f"""
|
||||
SELECT se.sag_id
|
||||
FROM sag_emails se
|
||||
JOIN email_messages em ON em.id = se.email_id
|
||||
WHERE em.deleted_at IS NULL
|
||||
AND LOWER(REGEXP_REPLACE(COALESCE(em.message_id, ''), '[<>\\s]', '', 'g')) IN ({placeholders})
|
||||
ORDER BY se.created_at DESC
|
||||
LIMIT 1
|
||||
""",
|
||||
tuple(thread_message_ids)
|
||||
)
|
||||
return rows[0]['sag_id'] if rows else None
|
||||
|
||||
def _find_customer_by_domain(self, domain: str) -> Optional[Dict[str, Any]]:
|
||||
if not domain:
|
||||
return None
|
||||
@ -309,6 +352,10 @@ class EmailWorkflowService:
|
||||
return None
|
||||
|
||||
sag_id = self._extract_sag_id(email_data)
|
||||
if not sag_id:
|
||||
sag_id = self._find_sag_id_from_thread_headers(email_data)
|
||||
if sag_id:
|
||||
logger.info("🔗 Matched email %s to SAG-%s via thread headers", email_id, sag_id)
|
||||
|
||||
# 1) Existing SAG via subject/headers
|
||||
if sag_id:
|
||||
|
||||
Loading…
Reference in New Issue
Block a user