Seth commited on
Commit
5720112
·
1 Parent(s): d2b0e4f
Files changed (1) hide show
  1. backend/app/main.py +243 -14
backend/app/main.py CHANGED
@@ -359,9 +359,156 @@ def _is_unipile_nonsense_display_name(val: str, account_id_hint: str = "") -> bo
359
  return True
360
  if account_id_hint and s == account_id_hint.strip():
361
  return True
 
 
362
  return False
363
 
364
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
365
  def _extract_unipile_identity_from_layer(data: dict, account_id_hint: str = "") -> tuple[str, str]:
366
  """Read display name + avatar from one object (e.g. GET /accounts/{id} payload)."""
367
  candidates_name = [
@@ -430,13 +577,47 @@ _MAILBOX_PROVIDER_SET = ("GOOGLE", "MICROSOFT", "OUTLOOK", "MAIL", "IMAP")
430
 
431
 
432
  def _mailbox_account_display_name(row: UnipileAccount) -> str:
433
- meta = row.metadata_json if isinstance(row.metadata_json, dict) else {}
434
- n = _extract_unipile_identity(meta)[0].strip()
 
 
 
435
  if n:
436
  return n
437
  return (row.label or "").strip() or "Mailbox"
438
 
439
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
440
  def _try_fetch_unipile_account_profile(account_id: str) -> dict:
441
  """
442
  Optional enrichment call. If it fails, we just keep existing metadata.
@@ -1316,6 +1497,8 @@ async def patch_me_linkedin_prefs(
1316
  async def unipile_mailbox_campaign_defaults(t: TenantContext = Depends(get_tenant_context)):
1317
  db = t.db
1318
  user_row = db.query(User).filter(User.id == t.user_id).first()
 
 
1319
  rows = (
1320
  db.query(UnipileAccount)
1321
  .filter(
@@ -1326,6 +1509,32 @@ async def unipile_mailbox_campaign_defaults(t: TenantContext = Depends(get_tenan
1326
  .order_by(UnipileAccount.created_at.desc(), UnipileAccount.id.desc())
1327
  .all()
1328
  )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1329
  accounts = []
1330
  for r in rows:
1331
  ident = _extract_unipile_identity(r.metadata_json or {})
@@ -1342,7 +1551,11 @@ async def unipile_mailbox_campaign_defaults(t: TenantContext = Depends(get_tenan
1342
  "created_at": r.created_at.isoformat() if r.created_at else None,
1343
  }
1344
  )
1345
- prof = (getattr(user_row, "mailbox_profile_display_name", None) or "").strip() if user_row else ""
 
 
 
 
1346
  default_ref = getattr(user_row, "default_mailbox_unipile_account_ref_id", None) if user_row else None
1347
  valid_default = None
1348
  if default_ref is not None and any(a["id"] == default_ref for a in accounts):
@@ -1365,7 +1578,7 @@ async def patch_me_mailbox_prefs(
1365
  raise HTTPException(status_code=404, detail="User not found")
1366
  patch = body.model_dump(exclude_unset=True)
1367
  if "mailbox_profile_display_name" in patch:
1368
- s = (patch["mailbox_profile_display_name"] or "").strip()
1369
  u.mailbox_profile_display_name = s[:200] if s else None
1370
  if "default_mailbox_unipile_account_ref_id" in patch:
1371
  rid = patch["default_mailbox_unipile_account_ref_id"]
@@ -1406,7 +1619,7 @@ async def create_unipile_mailbox_hosted_link(
1406
  expires_at = datetime.utcnow() + timedelta(minutes=20)
1407
  u = t.db.query(User).filter(User.id == t.user_id).first()
1408
  if u is not None and body.mailbox_profile_display_name is not None:
1409
- s = (body.mailbox_profile_display_name or "").strip()
1410
  if s:
1411
  u.mailbox_profile_display_name = s[:200]
1412
  state = UnipileHostedAuthState(
@@ -1482,9 +1695,16 @@ async def unipile_mailbox_hosted_callback(request: Request, db: Session = Depend
1482
  .first()
1483
  )
1484
  user_row = db.query(User).filter(User.id == state.user_id).first()
1485
- box_name = _extract_unipile_identity(merged_meta)[0].strip()
 
 
 
 
 
1486
  if user_row is not None and box_name:
1487
  user_row.mailbox_profile_display_name = box_name[:200]
 
 
1488
  nice_label = (state.label or "").strip() or "Mailbox"
1489
  if box_name:
1490
  nice_label = f"{box_name}'s mailbox"
@@ -1520,8 +1740,10 @@ async def unipile_mailbox_hosted_callback(request: Request, db: Session = Depend
1520
 
1521
  @app.get("/api/unipile/mailbox/accounts")
1522
  async def list_unipile_mailbox_accounts(t: TenantContext = Depends(get_tenant_context)):
 
 
1523
  rows = (
1524
- t.db.query(UnipileAccount)
1525
  .filter(
1526
  UnipileAccount.tenant_id == t.tenant_id,
1527
  UnipileAccount.user_id == t.user_id,
@@ -1530,6 +1752,15 @@ async def list_unipile_mailbox_accounts(t: TenantContext = Depends(get_tenant_co
1530
  .order_by(UnipileAccount.created_at.desc(), UnipileAccount.id.desc())
1531
  .all()
1532
  )
 
 
 
 
 
 
 
 
 
1533
  out = []
1534
  for r in rows:
1535
  ident = _extract_unipile_identity(r.metadata_json or {})
@@ -3924,16 +4155,14 @@ async def generate_sequences(
3924
  db.commit()
3925
 
3926
  user_row = db.query(User).filter(User.id == t.user_id).first()
3927
- mailbox_sig = ""
3928
  linkedin_sig = ""
3929
  if user_row:
3930
- mailbox_sig = (
3931
- (getattr(user_row, "mailbox_profile_display_name", None) or "").strip()
3932
- or (user_row.name or "").strip()
3933
- )
3934
  linkedin_sig = (
3935
- (getattr(user_row, "linkedin_profile_display_name", None) or "").strip()
3936
- or (user_row.name or "").strip()
 
 
3937
  )
3938
 
3939
  total_contacts = len(df)
 
359
  return True
360
  if account_id_hint and s == account_id_hint.strip():
361
  return True
362
+ if _looks_like_email(s):
363
+ return True
364
  return False
365
 
366
 
367
+ def _looks_like_email(val: str) -> bool:
368
+ s = (val or "").strip()
369
+ if not s or "@" not in s:
370
+ return False
371
+ return bool(re.match(r"^[^@\s]+@[^@\s]+\.[^@\s]+$", s))
372
+
373
+
374
+ def _display_name_from_email_local(email: str) -> str:
375
+ """Best-effort human label when UniPile only exposes the mailbox address (e.g. maya@co.com → Maya)."""
376
+ e = (email or "").strip()
377
+ if "@" not in e:
378
+ return ""
379
+ local = e.split("@", 1)[0].strip()
380
+ if not local:
381
+ return ""
382
+ parts = [p for p in re.split(r"[._+\-]+", local) if p and re.search(r"[A-Za-z]", p)]
383
+ if not parts:
384
+ return ""
385
+ return " ".join(p[:1].upper() + p[1:].lower() if len(p) > 1 else p.upper() for p in parts)
386
+
387
+
388
+ def _mailbox_email_from_unipile_meta(meta: Any) -> str:
389
+ if not isinstance(meta, dict):
390
+ return ""
391
+ for layer in (meta.get("account_profile"), meta):
392
+ if not isinstance(layer, dict):
393
+ continue
394
+ cp = layer.get("connection_params")
395
+ if isinstance(cp, dict):
396
+ mail = cp.get("mail")
397
+ if isinstance(mail, dict):
398
+ for key in ("username", "imap_user", "smtp_user", "email"):
399
+ v = _safe_str(mail.get(key))
400
+ if _looks_like_email(v):
401
+ return v
402
+ for key in ("email", "username"):
403
+ v = _safe_str(layer.get(key))
404
+ if _looks_like_email(v):
405
+ return v
406
+ n = _safe_str(layer.get("name"))
407
+ if _looks_like_email(n):
408
+ return n
409
+ return ""
410
+
411
+
412
+ def _name_from_mailbox_label(label: str) -> str:
413
+ lab = (label or "").strip()
414
+ suffix = "'s mailbox"
415
+ if lab.endswith(suffix) and len(lab) > len(suffix):
416
+ cand = lab[: -len(suffix)].strip()
417
+ if cand and not _looks_like_email(cand) and not _is_unipile_nonsense_display_name(cand):
418
+ return cand
419
+ return ""
420
+
421
+
422
+ def _try_fetch_mailbox_sender_display_name(account_id: str, mailbox_email: str = "") -> str:
423
+ """
424
+ Gmail 'Send mail as' display name often appears on sent messages as from_attendee.display_name.
425
+ UniPile account `name` is frequently the mailbox address only.
426
+ """
427
+ if not (account_id or "").strip():
428
+ return ""
429
+ acc_enc = requests.utils.quote(account_id.strip(), safe="")
430
+ want = (mailbox_email or "").strip().lower()
431
+ folder_candidates = ("SENT", "sent", "Sent", "Sent Items", "INBOX")
432
+ for folder in folder_candidates:
433
+ folder_enc = requests.utils.quote(folder, safe="")
434
+ st, data = _unipile_call(
435
+ "GET",
436
+ f"/api/v1/emails?account_id={acc_enc}&limit=15&folder={folder_enc}",
437
+ )
438
+ if st >= 400 or not isinstance(data, dict):
439
+ continue
440
+ items = data.get("items") or data.get("emails") or data.get("data") or []
441
+ if not isinstance(items, list):
442
+ continue
443
+ for item in items:
444
+ if not isinstance(item, dict):
445
+ continue
446
+ fa = item.get("from_attendee") or item.get("from") or {}
447
+ if not isinstance(fa, dict):
448
+ continue
449
+ dn = _safe_str(fa.get("display_name") or fa.get("name"))
450
+ ident = _safe_str(fa.get("identifier") or fa.get("email") or fa.get("address"))
451
+ if not dn or _looks_like_email(dn):
452
+ continue
453
+ if want and ident and ident.lower() != want:
454
+ continue
455
+ return dn
456
+ return ""
457
+
458
+
459
+ def _resolve_mailbox_sender_name(
460
+ meta: Any,
461
+ *,
462
+ unipile_account_id: str = "",
463
+ label: str = "",
464
+ fetch_sent: bool = True,
465
+ ) -> str:
466
+ """Human sender name for mailbox UI + email sign-offs — never the raw mailbox address when avoidable."""
467
+ mailbox_email = _mailbox_email_from_unipile_meta(meta)
468
+ name = _extract_unipile_identity(meta if isinstance(meta, dict) else {})[0].strip()
469
+ if name and not _looks_like_email(name):
470
+ return name
471
+ if fetch_sent and unipile_account_id:
472
+ from_sent = _try_fetch_mailbox_sender_display_name(unipile_account_id, mailbox_email)
473
+ if from_sent:
474
+ return from_sent
475
+ from_label = _name_from_mailbox_label(label)
476
+ if from_label:
477
+ return from_label
478
+ if mailbox_email:
479
+ derived = _display_name_from_email_local(mailbox_email)
480
+ if derived:
481
+ return derived
482
+ return ""
483
+
484
+
485
+ def _sanitize_sender_signoff_name(val: str) -> str:
486
+ s = (val or "").strip()
487
+ if not s or _looks_like_email(s):
488
+ return ""
489
+ return s
490
+
491
+
492
+ def _mailbox_sender_name_for_user(db: Session, user_row: Optional[User]) -> str:
493
+ if not user_row:
494
+ return ""
495
+ pref = _sanitize_sender_signoff_name(getattr(user_row, "mailbox_profile_display_name", None) or "")
496
+ if pref:
497
+ return pref
498
+ ref_id = getattr(user_row, "default_mailbox_unipile_account_ref_id", None)
499
+ if ref_id is not None:
500
+ row = db.query(UnipileAccount).filter(UnipileAccount.id == int(ref_id)).first()
501
+ if row:
502
+ resolved = _resolve_mailbox_sender_name(
503
+ row.metadata_json or {},
504
+ unipile_account_id=row.unipile_account_id or "",
505
+ label=row.label or "",
506
+ )
507
+ if resolved:
508
+ return resolved
509
+ return _sanitize_sender_signoff_name(user_row.name or "")
510
+
511
+
512
  def _extract_unipile_identity_from_layer(data: dict, account_id_hint: str = "") -> tuple[str, str]:
513
  """Read display name + avatar from one object (e.g. GET /accounts/{id} payload)."""
514
  candidates_name = [
 
577
 
578
 
579
  def _mailbox_account_display_name(row: UnipileAccount) -> str:
580
+ n = _resolve_mailbox_sender_name(
581
+ row.metadata_json if isinstance(row.metadata_json, dict) else {},
582
+ unipile_account_id=row.unipile_account_id or "",
583
+ label=row.label or "",
584
+ )
585
  if n:
586
  return n
587
  return (row.label or "").strip() or "Mailbox"
588
 
589
 
590
+ def _refresh_mailbox_account_identity(
591
+ db: Session,
592
+ row: UnipileAccount,
593
+ user_row: Optional[User],
594
+ *,
595
+ update_user_pref: bool = False,
596
+ ) -> None:
597
+ """Re-fetch UniPile profile / sent-mail display name when stored metadata only has the email address."""
598
+ account_id = row.unipile_account_id or ""
599
+ if not account_id:
600
+ return
601
+ profile = _try_fetch_unipile_account_profile(account_id)
602
+ meta = dict(row.metadata_json) if isinstance(row.metadata_json, dict) else {}
603
+ if isinstance(profile, dict) and profile:
604
+ meta["account_profile"] = profile
605
+ resolved = _resolve_mailbox_sender_name(
606
+ meta,
607
+ unipile_account_id=account_id,
608
+ label=row.label or "",
609
+ fetch_sent=True,
610
+ )
611
+ row.metadata_json = meta
612
+ if resolved:
613
+ row.label = f"{resolved}'s mailbox"[:255]
614
+ row.updated_at = datetime.utcnow()
615
+ if update_user_pref and user_row is not None:
616
+ cur = _sanitize_sender_signoff_name(getattr(user_row, "mailbox_profile_display_name", None) or "")
617
+ if not cur:
618
+ user_row.mailbox_profile_display_name = resolved[:200]
619
+
620
+
621
  def _try_fetch_unipile_account_profile(account_id: str) -> dict:
622
  """
623
  Optional enrichment call. If it fails, we just keep existing metadata.
 
1497
  async def unipile_mailbox_campaign_defaults(t: TenantContext = Depends(get_tenant_context)):
1498
  db = t.db
1499
  user_row = db.query(User).filter(User.id == t.user_id).first()
1500
+ if user_row is not None and _looks_like_email(getattr(user_row, "mailbox_profile_display_name", None) or ""):
1501
+ user_row.mailbox_profile_display_name = None
1502
  rows = (
1503
  db.query(UnipileAccount)
1504
  .filter(
 
1509
  .order_by(UnipileAccount.created_at.desc(), UnipileAccount.id.desc())
1510
  .all()
1511
  )
1512
+ default_ref = getattr(user_row, "default_mailbox_unipile_account_ref_id", None) if user_row else None
1513
+ identity_dirty = False
1514
+ for r in rows:
1515
+ meta = r.metadata_json if isinstance(r.metadata_json, dict) else {}
1516
+ raw_identity = _extract_unipile_identity(meta)[0]
1517
+ resolved_quick = _resolve_mailbox_sender_name(
1518
+ meta,
1519
+ unipile_account_id=r.unipile_account_id or "",
1520
+ label=r.label or "",
1521
+ fetch_sent=False,
1522
+ )
1523
+ if r.unipile_account_id and (_looks_like_email(raw_identity) or not resolved_quick):
1524
+ _refresh_mailbox_account_identity(
1525
+ db,
1526
+ r,
1527
+ user_row,
1528
+ update_user_pref=(
1529
+ default_ref is not None and int(default_ref) == int(r.id)
1530
+ or (default_ref is None and rows and int(r.id) == int(rows[0].id))
1531
+ ),
1532
+ )
1533
+ identity_dirty = True
1534
+ if identity_dirty:
1535
+ db.commit()
1536
+ if user_row is not None:
1537
+ db.refresh(user_row)
1538
  accounts = []
1539
  for r in rows:
1540
  ident = _extract_unipile_identity(r.metadata_json or {})
 
1551
  "created_at": r.created_at.isoformat() if r.created_at else None,
1552
  }
1553
  )
1554
+ prof = _sanitize_sender_signoff_name(
1555
+ (getattr(user_row, "mailbox_profile_display_name", None) or "").strip() if user_row else ""
1556
+ )
1557
+ if not prof and user_row:
1558
+ prof = _mailbox_sender_name_for_user(db, user_row)
1559
  default_ref = getattr(user_row, "default_mailbox_unipile_account_ref_id", None) if user_row else None
1560
  valid_default = None
1561
  if default_ref is not None and any(a["id"] == default_ref for a in accounts):
 
1578
  raise HTTPException(status_code=404, detail="User not found")
1579
  patch = body.model_dump(exclude_unset=True)
1580
  if "mailbox_profile_display_name" in patch:
1581
+ s = _sanitize_sender_signoff_name((patch["mailbox_profile_display_name"] or "").strip())
1582
  u.mailbox_profile_display_name = s[:200] if s else None
1583
  if "default_mailbox_unipile_account_ref_id" in patch:
1584
  rid = patch["default_mailbox_unipile_account_ref_id"]
 
1619
  expires_at = datetime.utcnow() + timedelta(minutes=20)
1620
  u = t.db.query(User).filter(User.id == t.user_id).first()
1621
  if u is not None and body.mailbox_profile_display_name is not None:
1622
+ s = _sanitize_sender_signoff_name((body.mailbox_profile_display_name or "").strip())
1623
  if s:
1624
  u.mailbox_profile_display_name = s[:200]
1625
  state = UnipileHostedAuthState(
 
1695
  .first()
1696
  )
1697
  user_row = db.query(User).filter(User.id == state.user_id).first()
1698
+ box_name = _resolve_mailbox_sender_name(
1699
+ merged_meta,
1700
+ unipile_account_id=unipile_account_id,
1701
+ label=(state.label or "").strip(),
1702
+ fetch_sent=True,
1703
+ )
1704
  if user_row is not None and box_name:
1705
  user_row.mailbox_profile_display_name = box_name[:200]
1706
+ elif user_row is not None and _looks_like_email(getattr(user_row, "mailbox_profile_display_name", None) or ""):
1707
+ user_row.mailbox_profile_display_name = None
1708
  nice_label = (state.label or "").strip() or "Mailbox"
1709
  if box_name:
1710
  nice_label = f"{box_name}'s mailbox"
 
1740
 
1741
  @app.get("/api/unipile/mailbox/accounts")
1742
  async def list_unipile_mailbox_accounts(t: TenantContext = Depends(get_tenant_context)):
1743
+ db = t.db
1744
+ user_row = db.query(User).filter(User.id == t.user_id).first()
1745
  rows = (
1746
+ db.query(UnipileAccount)
1747
  .filter(
1748
  UnipileAccount.tenant_id == t.tenant_id,
1749
  UnipileAccount.user_id == t.user_id,
 
1752
  .order_by(UnipileAccount.created_at.desc(), UnipileAccount.id.desc())
1753
  .all()
1754
  )
1755
+ identity_dirty = False
1756
+ for r in rows:
1757
+ meta = r.metadata_json if isinstance(r.metadata_json, dict) else {}
1758
+ raw_identity = _extract_unipile_identity(meta)[0]
1759
+ if r.unipile_account_id and _looks_like_email(raw_identity):
1760
+ _refresh_mailbox_account_identity(db, r, user_row, update_user_pref=False)
1761
+ identity_dirty = True
1762
+ if identity_dirty:
1763
+ db.commit()
1764
  out = []
1765
  for r in rows:
1766
  ident = _extract_unipile_identity(r.metadata_json or {})
 
4155
  db.commit()
4156
 
4157
  user_row = db.query(User).filter(User.id == t.user_id).first()
4158
+ mailbox_sig = _mailbox_sender_name_for_user(db, user_row)
4159
  linkedin_sig = ""
4160
  if user_row:
 
 
 
 
4161
  linkedin_sig = (
4162
+ _sanitize_sender_signoff_name(
4163
+ (getattr(user_row, "linkedin_profile_display_name", None) or "").strip()
4164
+ )
4165
+ or _sanitize_sender_signoff_name((user_row.name or "").strip())
4166
  )
4167
 
4168
  total_contacts = len(df)