Skip to content

feat(common-contacts): suggest recipients from the stack - #4998

Open
MontaGhanmy wants to merge 11 commits into
masterfrom
feat/common-contacts-suggest
Open

MontaGhanmy wants to merge 11 commits into
masterfrom
feat/common-contacts-suggest

Conversation

@MontaGhanmy

@MontaGhanmy MontaGhanmy commented Oct 13, 2026 •

Copy link
Copy Markdown
Contributor

Summary

  • New route GET /contacts/suggest?q=&limit=, with a GET permission on the whole io.cozy.contacts doctype. q needs 3 characters, limit defaults to 20 and goes up to 256.
  • When common_contacts is enabled and the context is set in contacts_search, the stack asks the autocomplete API of the calendar side service, on behalf of the email of the instance, for people with an email. The call times out after 1 second.
  • Each result is looked up by email on the org instance, then on the user's instance, with one request each, trashed contacts aside. The response keeps the order of the search and carries meta.kind: member when found on the org instance, contact on the user's instance. A result with no document comes back without id and meta, with its name and email.
  • With common_contacts disabled, without contacts_search, or when the call fails or times out (logged), the stack runs a case-insensitive $regex Mango query on names and emails, on both instances. Trashed contacts and contacts without an email are left out, and a member copied to the user's instance comes back once, as a member.
  • The user and duplicate emails are left out.
  • Resolve email addresses regardless of stored casing. The email view indexes normalized addresses and retains original keys for older clients; the index version is bumped so existing instances upgrade on access. Sharing by email reuses the organization contact and its Cozy URL.
  • contacts_search is a top-level section keyed by context, like common_settings, and not under contexts: GET /settings/context returns the context to any token, so the secret would leak. The ADR (docs: add contact suggestions adr #4915) needs the same change.
  • The side service ignores hasFields until [COMMON CONTACT] hasField support twake-calendar-side-service#1109. Until then the stack drops results without an email, so a response can be shorter than limit.
  • Open question: the timeout, waiting on the latency question of the ADR.
  • Local organization lookup remains available with both flags disabled. External search alone needs common_contacts; it does not change directory replication. The first backend rollout retains replication and leaves the existing sharing UI unchanged.
  • Depends on feat(common-contacts): groups on the organization instance #4976.

Part of linagora/twake-workplace-private#1926, linagora/twake-workplace-private#1927 and linagora/twake-workplace-private#1928.

@MontaGhanmy
MontaGhanmy force-pushed the feat/common-contacts-suggest branch from e50671a to 7e9e554 Compare October 14, 2026 09:32
@MontaGhanmy
MontaGhanmy marked this pull request as ready for review October 14, 2026 15:15
@MontaGhanmy
MontaGhanmy force-pushed the feat/common-contacts-suggest branch from 7e9e554 to fdab2a3 Compare October 14, 2026 15:40
@MontaGhanmy
MontaGhanmy force-pushed the feat/common-contacts-groups-on-org branch from 25fffaa to 0a27e1f Compare October 14, 2026 15:40

@rezk2ll rezk2ll left a comment •

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Two things I'd fix: the email lookup is case sensitive (it creates duplicate contacts), and the fallback scans the whole org on every keystroke. Details inline.

Comment thread model/contact/search.go
}
found[email] = doc
}
return found, nil

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The contacts-by-email view emits addresses as stored, so Bob@Acme.example misses bob@acme.example.

Locally the suggestion came back without id or meta. Sharing to it then created a duplicate "Bob" contact with no instance URL, so a mail invite instead of cozy to cozy.

Querying both the address and its lowercase form, and keying the map by strings.ToLower, should do it. Same lookup in model/sharing/suggest.go L63-L70.

Comment thread model/contact/search.go
return nil, nil
}
return docs, err
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Two things here.

First, $regex through FindDocsUnoptimized is a full scan of the org instance, then of the user's instance, on every keystroke. With 20k members a query with no match takes 0.6s locally. Could the two calls run in parallel under a deadline? Or at least the docs could say contacts_search is expected in prod.

Second, CouchDB compiles the regex without UTF8, so (?i) only folds ASCII. q=élo misses Élodie. "(*UTF8)(?i)" + regexp.QuoteMeta(text) fixes it, and an accented test would be nice.

@MontaGhanmy MontaGhanmy Oct 20, 2026 •

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Accents are fixed. For the scan: the side service is the main path (#4915), the regex is the degraded mode linagora/twake-workplace-private#1928 asked for, when contacts_search isn't set or the side service is down. It stops at limit matches, so the full scan is only the no-match case you measured.

Comment thread model/sharing/suggest.go
}
for _, c := range contacts {
add(c, SuggestionContact)
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: members fill the limit first. With 20 matching members the user's own contact never shows up.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

On purpose, members go first so a member copied to the instance comes back as a member. It only affects the fallback: with the side service, the results keep its order.

Comment thread web/contacts/contacts.go
delete(s.Contact.M, "_rev")
data[i].Attributes = s.Contact.M
data[i].Meta = map[string]string{"kind": s.Kind}
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This returns the whole org contact doc (phone, notes, carddavPath...) to any token with GET io.cozy.contacts on a member instance. Should it whitelist fullname, name, email and cozy?

Also, trashed contacts get suggested. Should they be filtered out?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Trashed ones are filtered now. For the fields, nothing new: until #4970 every member was copied into each instance with name, email, phone and cozy URL, readable by the same token. The feed only adds carddavPath, no notes.

t.Cleanup(ts.Close)
config.GetConfig().ContactsSearch = map[string]config.ContactsSearch{
"acme": {URL: ts.URL + "/api/people/search", Token: "s3cr3t"},
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: ContactsSearch is set here without a t.Cleanup reset.

@shepilov
shepilov force-pushed the feat/common-contacts-groups-on-org branch from 7337a6e to 8fc77f2 Compare October 27, 2026 16:55
@shepilov
shepilov force-pushed the feat/common-contacts-suggest branch from eb885f5 to 6876dfc Compare October 27, 2026 16:55
@shepilov
shepilov force-pushed the feat/common-contacts-groups-on-org branch 3 times, most recently from e81976f to d6bfe5e Compare October 27, 2026 18:35
Base automatically changed from feat/common-contacts-groups-on-org to master October 27, 2026 18:37
@shepilov
shepilov force-pushed the feat/common-contacts-suggest branch from 6876dfc to 36b88ed Compare October 27, 2026 18:40

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants