Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support cyrus server #3457

Open
wants to merge 16 commits into
base: master
Choose a base branch
from
Open

Support cyrus server #3457

wants to merge 16 commits into from

Conversation

florentos17
Copy link
Collaborator

tackling issue 2305

each commit has a self-explanatory message, but do not hesitate if you have any question !

i have tested the changes with both tmail-backend and cyrus to check that the expected behaviour happens, and it looks all good to me.

Copy link

This PR has been deployed to https://linagora.github.io/tmail-flutter/3457.

@florentos17
Copy link
Collaborator Author

florentos17 commented Jan 30, 2025

the force push was to fix the 'unused import' warnings and prefix each commit message with TF-2305

@florentos17
Copy link
Collaborator Author

regarding the way URIExtension::toQualifiedUrl works: i did some adjustments to the unit tests because i feel like the wrong behaviour was expected: to me, if a trailing slash is advertized, then it should remain. I say this because it is the way cyrus works, but i dont know if this is universal so definitely tell me what you think about that.

with the current changes, tmail-flutter works well with tmail-backend and with cyrus. this issue (Session: handle relative URL) mentions compatibility with fastmail. i dont have a fastmail server at hand but if you have a URL for one i'd be happy to test !

try {
var principalsCapability = getCapabilityProperties<DefaultCapability>(
AccountId(Id(username.value)),
CapabilityIdentifier(Uri.parse('urn:ietf:params:jmap:principals')));
Copy link
Member

Choose a reason for hiding this comment

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

Please define this capability

Copy link
Member

Choose a reason for hiding this comment

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

I did not find it in spec. It is Cyrus customization? Can you share session example of Cyrus here?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

here is one of the accountCapabilities from the cyrus session:

"urn:ietf:params:jmap:principals": {
  "currentUserPrincipalId": "bob",
  "urn:ietf:params:jmap:calendars": {
    "accountId": "bob",
    "account": null,
    "mayGetAvailability": true,
    "sendTo": {
      "imip": "mailto:[email protected]"
    }
  }
},

it is the only place where the email address appears in the session, so benoit suggested in this comment to fetch the address from there if necessary

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

@hoangdat what do you think ? Indeed I dont think there is any doc about fetching the email from there ; the idea comes from @chibenwa 's comment

@@ -53,6 +52,28 @@ extension SessionExtension on Session {
}
}

String? getEmailAddress() {
Copy link
Member

Choose a reason for hiding this comment

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

We also should write unit test for this function

Copy link
Member

Choose a reason for hiding this comment

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

I still prefer getUsername() or getOwnEmailAddress()?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Unit tests done !

If we want to get the username, we should simply return session.username. This method is used to retrieve the full email address (sometimes used as the username, sometimes not). Do you want me to change getEmailAddress() to getOwnEmailAddress() ?

Copy link
Member

Choose a reason for hiding this comment

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

term email address is not specific for this. The reason for usename is the current account use the app, we need to distinguish with other email addresses in delegate mode or multiple account. But ok with getOwnEmailAddress()

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

ok done, i changed the name !

lib/features/thread/presentation/thread_view.dart Outdated Show resolved Hide resolved
Comment on lines 20 to 22
String getDownloadUrl(String jmapUrl) {
final downloadUrlValid = downloadUrl.toQualifiedUrl(baseUrl: Uri.parse(jmapUrl));
Copy link
Contributor

Choose a reason for hiding this comment

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

Why is there a need to change it?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

because

  1. the jmapUrl attribute cannot be defaulted when using a cyrus server: it is not advertized in the session ;
  2. it was already always provided anyway.

Copy link
Member

Choose a reason for hiding this comment

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

the jmapUrl attribute cannot be defaulted when using a cyrus server: it is not advertized in the session

Not really understand it.
Moreover, we also support other back-end: Stalwart, Cyrus, James. Keep it optional will keep flexible.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Previously the base URL jmapUrl attribute was optional and the default value was the downloadUrl provided in the session. For Tmail-backend, this is a full URL (so it's ok), but for Cyrus it is only the relative route:

  "downloadUrl": "/jmap/download/{accountId}/{blobId}/{name}?accept={type}",
  "uploadUrl": "/jmap/upload/{accountId}/",

but I also made changes to the toQualifiedUrl method so that both cases are handled.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Thinking back about it, you are right that keeping it optional will keep it flexible, it is better.

On my latest commit you can see that I reverted it, but then I had to handle the case where the URL cannot be found at all by throwing an exception, is that ok ?

Comment on lines 30 to 32
Uri getUploadUri(AccountId accountId, String jmapUrl) {
final uploadUrlValid = uploadUrl.toQualifiedUrl(baseUrl: Uri.parse(jmapUrl));
Copy link
Contributor

Choose a reason for hiding this comment

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

idem

Copy link
Member

Choose a reason for hiding this comment

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

we also support other back-end: Stalwart, Cyrus, James. Keep it optional will keep flexible.

lib/features/thread/presentation/thread_controller.dart Outdated Show resolved Hide resolved
@dab246
Copy link
Member

dab246 commented Feb 4, 2025

@florentos17 Please confirm Done in the comments that you have fixed it. Thanks

@@ -20,7 +20,7 @@ void main() {
final baseUrl = Uri.parse('https://domain.com');
final sourceUrl = Uri.parse('/jmap/');

final qualifiedUrlExpected = Uri.parse('https://domain.com/jmap');
final qualifiedUrlExpected = Uri.parse('https://domain.com/jmap/');
Copy link
Member

Choose a reason for hiding this comment

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

Why we need this? In what cases?

IMO, it is our normalization, please keep it in uniform.

Copy link
Collaborator Author

@florentos17 florentos17 Feb 4, 2025

Choose a reason for hiding this comment

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

cyrus advertizes the following in the session: "uploadUrl": "/jmap/upload/{accountId}/"

when querying https://localhost/jmap/upload/bob/, the upload works, but with https://localhost/jmap/upload/bob (without the trailing /), I get a 404 Not Found error.

Copy link
Member

@hoangdat hoangdat Feb 6, 2025

Choose a reason for hiding this comment

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

okie, in this case, make it uniform, keep / for all qualified url, and update all the tests too.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

But tmail-backend works the other way around: no / advertized, no / needed, and it does not work if the / is there...

So I feel like the solution is to keep the / if it is advertized, and not add it if not advertized ? But I agree that it is not uniform.

log('SessionUtils::toQualifiedUrl():baseUrlValid: $baseUrlValid | sourceUrlValid: $sourceUrlValid');
final qualifiedUrl = baseUrlValid + sourceUrlValid;
log('SessionUtils::toQualifiedUrl():qualifiedUrl: $qualifiedUrl');
return Uri.parse(qualifiedUrl);
Copy link
Member

Choose a reason for hiding this comment

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

What happens if parse failed? Can we use tryParse instead?

Copy link
Member

Choose a reason for hiding this comment

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

any update? @florentos17

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

What default behaviour would you expect if the parse fails and null is returned ?

The methods that call toQualifiedUrl (getQualifiedApiUrl, getDownloadUrl, getUploadUrl) mostly cannot work with null, so errors have to be thrown. But then it would be easier to just let toQualifiedUrl throw the error ?

Comment on lines 20 to 22
String getDownloadUrl(String jmapUrl) {
final downloadUrlValid = downloadUrl.toQualifiedUrl(baseUrl: Uri.parse(jmapUrl));
Copy link
Member

Choose a reason for hiding this comment

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

the jmapUrl attribute cannot be defaulted when using a cyrus server: it is not advertized in the session

Not really understand it.
Moreover, we also support other back-end: Stalwart, Cyrus, James. Keep it optional will keep flexible.

Comment on lines 30 to 32
Uri getUploadUri(AccountId accountId, String jmapUrl) {
final uploadUrlValid = uploadUrl.toQualifiedUrl(baseUrl: Uri.parse(jmapUrl));
Copy link
Member

Choose a reason for hiding this comment

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

we also support other back-end: Stalwart, Cyrus, James. Keep it optional will keep flexible.

try {
var principalsCapability = getCapabilityProperties<DefaultCapability>(
AccountId(Id(username.value)),
CapabilityIdentifier(Uri.parse('urn:ietf:params:jmap:principals')));
Copy link
Member

Choose a reason for hiding this comment

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

I did not find it in spec. It is Cyrus customization? Can you share session example of Cyrus here?

@@ -53,6 +52,28 @@ extension SessionExtension on Session {
}
}

String? getEmailAddress() {
Copy link
Member

Choose a reason for hiding this comment

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

I still prefer getUsername() or getOwnEmailAddress()?

Comment on lines 64 to 67
String wrappedAddress = ((((principalsCapability?.properties
?.values.last) as Map<String, dynamic>)
.values.last) as Map<String, dynamic>)
.values.toString();
Copy link
Member

Choose a reason for hiding this comment

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

not a fan of hard force casting and prefer strong typed for this

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

something like that ?

final sendTo = principalsCapability?.properties?['urn:ietf:params:jmap:calendars']?['sendTo'];
if (sendTo is Map<String, dynamic>) {
  final wrappedAddress = sendTo['imip'];
  if (wrappedAddress is String && wrappedAddress.startsWith('mailto:')) {
    String address = wrappedAddress.substring("mailto:".length);
    return address.isEmail ? address : null;
  }
}

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

is that ok @hoangdat ?

lib/features/thread/presentation/thread_controller.dart Outdated Show resolved Hide resolved
tddang-linagora
tddang-linagora previously approved these changes Feb 4, 2025
@hoangdat
Copy link
Member

hoangdat commented Feb 5, 2025

hi @florentos17, I rebased the branch from customer branch, so some files conflict. Please try to help us resolve it.

This ticket is the first priority. please let us know when you are done in resolving conflict and comments.

Thanks

@florentos17
Copy link
Collaborator Author

florentos17 commented Feb 5, 2025

@hoangdat when you forced pushed on master, it "added" 19 commits to this PR: commits that were previously on the master branch but are not anymore since the force push.

among these 19 commits:

  • 12 are actually already on the new master (just a different hash), so i will force push on this branch to remove them from the current PR
  • 7 are not on the new master at all... could that be an error ? here they are:
47b842d25 TF-3189 composer now correctly encodes subaddresses
20c4720c3 TF-3189 new `reply to` field in the mail composer
d939ffb1c TF-3189 new confirmation popup when enabling subaddressing for a folder
0f61e3de8 TF-3189 subaddressing features only shown if supported by the server
810e304c1 TF-3189 new option to copy a folder's subaddress
ed0b14c35 TF-3189 new option to enable/disable subaddressing for a personal folderc
...
1a58542ab TF-3265 Fix double scrolling composer

do you want me to leave them on the current PR so that they are reinstated ? or maybe cherry-pick them back yourself onto master ?

@@ -2755,7 +2755,7 @@ class MailboxDashBoardController extends ReloadableController
dispatchAction(SelectionAllEmailAction());
}

String get baseDownloadUrl => sessionCurrent?.getDownloadUrl(jmapUrl: dynamicUrlInterceptors.jmapUrl) ?? '';
String get baseDownloadUrl => sessionCurrent?.getDownloadUrl(dynamicUrlInterceptors.jmapUrl!) ?? '';
Copy link
Member

Choose a reason for hiding this comment

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

not a big fan of ! without pre-check

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

done, i reverted the change

@@ -530,7 +530,7 @@ class SingleEmailController extends BaseController with AppLoaderMixin {
)));
} else {
if (session != null && accountId != null) {
final baseDownloadUrl = session!.getDownloadUrl(jmapUrl: dynamicUrlInterceptors.jmapUrl);
final baseDownloadUrl = session!.getDownloadUrl(dynamicUrlInterceptors.jmapUrl!);
Copy link
Member

Choose a reason for hiding this comment

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

not safety: ! without pre-check

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

done, i reverted the change

.values.last) as Map<String, dynamic>)
.values.toString();

String address = wrappedAddress.substring("(mailto:".length, wrappedAddress.length - ")".length);
Copy link
Member

Choose a reason for hiding this comment

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

Still not find the docs said that this field can use as email address of current account. Can you more details?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

i answered on one of your earlier comments: #3457 (comment)

CancelToken? cancelToken,
}) async* {
try {
if (uploadUri == null) throw UnknownUriException();
Copy link
Member

Choose a reason for hiding this comment

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

We should not put the uploadUri == null check in this interactor. It is against the interactor's functionality. You should check it before calling this interactor. uploadUri is required in this interactor.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

This is what I did at first, but it means the interactor is not even called in case of a null uploadUri, so there is no UploadAttachmentFailure, and the red toast notification "upload attachment failed" does not show on the app.

I thought it would be better for the UX to have the red toast notification show up, but I understand the concern. Should I change it to put the check before ? Maybe I can throw a GetUploadUriFailure earlier to still have the toast notification ?

Copy link
Member

Choose a reason for hiding this comment

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

Yes, you check uploadUri != null before calling the interactor. And throw UploadAttachmentFailure(UnknownUriException()) exception as soon as you check it for null

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

done !

} else if (uploadUrl.hasOrigin) {
uploadUrlValid = uploadUrl;
} else {
return null;
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
return null;
throw UnknownUriException();

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

done !

.map((identity) => identity.toEmailAddressNoName())
.toSet()
.toList();

listEmailAddressDefault.add(EmailAddress(null, session?.getOwnEmailAddress()));
Copy link
Member

Choose a reason for hiding this comment

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

why do we need it?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

cyrus does not advertize the address like tmail does, so without the change the list of possible addresses is empty (clicking on the widget does nothing):
image

with the change, the default address appears:
image

and I checked, it does not duplicate the address if it was already there when using tmail

Copy link
Member

Choose a reason for hiding this comment

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

So you should check if listEmailAddressDefault.isEmpty then add getOwnEmailAddress. Avoid duplicate emails.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

no, it is already good, there is already no duplication.
i can add a listEmailAddressDefault.isEmpty but it makes no difference

Comment on lines +98 to +103
String wrappedAddress = ((((principalsCapability?.properties
?.values.last) as Map<String, dynamic>)
.values.last) as Map<String, dynamic>)
.values.toString();

String address = wrappedAddress.substring("(mailto:".length, wrappedAddress.length - ")".length);
Copy link
Member

Choose a reason for hiding this comment

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

IMO, Getting the email this way is inefficient, because we don't know exactly which field contains the email, the order of the keys of the returned json will be different on the servers, so truncating the string of the last element is redundant. So we should skip this.

Copy link
Collaborator Author

@florentos17 florentos17 Feb 7, 2025

Choose a reason for hiding this comment

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

the issue is that this is the only place where the full email address is advertized by cyrus: without this, the address wil be unknown.

final sendTo = principalsCapability?.properties?['urn:ietf:params:jmap:calendars']?['sendTo'];
if (sendTo is Map<String, dynamic>) {
  final wrappedAddress = sendTo['imip'];
  if (wrappedAddress is String && wrappedAddress.startsWith('mailto:')) {
    String address = wrappedAddress.substring("mailto:".length);
    return address.isEmail ? address : null;
  }
}

would that be ok ?

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.

4 participants