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

Send via SMTP support for aliases #869

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

Conversation

sahilph
Copy link

@sahilph sahilph commented Apr 2, 2022

This PR add ability to send via SMTP for aliases. With this emails can be sent directly from aliases via SMTP without the need for creation of reverse-alias.

If enabled, Every alias will have a unique password (generated client side) for SMTP access. Username will be same as alias.

This adds a global option in Settings to enable this feature, without enabling this option, SMTP access will remain disabled for the user.

Note: Even after enabling this option in settings, you would still need to enable SMTP individually per alias. This will also disable receiving via reverse-alias for that particular alias. i.e. Sender's original address will be displayed in "From:".

SMTP server address should ideally be the same where your app is hosted (but this depends on your configuration)
SMTP port: 465
This would need SSL cert/key file as SMTP would be over (implicit) SSL.

This should resolve #679

The screenshots below will make it more clear on how it would operate:

  • First enable "SMTP for Aliases" from Settings Page:

image

  • Then on the Aliases page, select "More" on an alias and a new toggle, to enable SMTP, will have appeared below "Pin this alias":

image

  • After toggling it, SMTP credential will be generated for that alias:

image

  • This will also disable receiving via reverse-alias, for that particular alias. i.e. Sender's original address will be displayed in "From:"

@developStorm
Copy link
Contributor

Great job! Thanks @sahilph. I think this is definitely a valuable feature. The only point I currently find somewhat awkward is that each alias would get their own credentials. My thought for this feature is that users should be able to connect to the SMTP server using a generic credential, and the SMTP server would automatically detect which specific alias should be used to send the email by looking at the address the user is replying to, and other information that available. This way, users would only have to add their credentials once to Gmail's send-as feature or local email client, and reply to emails sent to any alias with the generic credentials.

@sahilph
Copy link
Author

sahilph commented Apr 2, 2022

@developStorm Thanks for you feedback.

One of the main reasons I developed this was to use Gmail "Send mail as:" feature. And It requires users to add "From:", and username and password for every alias.
So even if there was a single generic credential, users will have to manually add the alias in Gmail anyways. So I thought of adding separate credentials, which would make the authentication handling easier.

Also consider one more scenario. Suppose the aliases are getting generated on-the-fly and I want to give a email to website say non-existent-alias@mycustom.domain.
Now non-existent-alias@mycustom.domain is not the alias which I ever send email from, but for some reason the website needs replying from that email, even if there was generic credential then I would have to again add that alias in Gmail's "Send mail as:" anyways.

In the current scenario, since we can enable SMTP per-alias-basis, a reverse-alias will automatically get generated and replying will be easier as I can just reply via reverse-alias.

Also I'm not sure how to authenticate and validate, if there was only a single generic credential present. If you have any ideas please let me know and I can implement it.

@developStorm
Copy link
Contributor

@sahilph The idea is that the SMTP server does not need to honor the From specified in "Send mail as:" feature. So we can just put a dummy From there and let the SMTP server figrue out which From (alias) should be used based on the mail content that is being sent. How to determine right alias to use is a problem though, I'm thinking that we can use the recipent's address but I'm not sure if that's necessarily accurate. Any thoughts on this?

@sahilph
Copy link
Author

sahilph commented Apr 2, 2022

@developStorm While it would be very convenient to have such a feature, I can't really think of a way of implementing this without reading From: field 😅.

@developStorm
Copy link
Contributor

developStorm commented Apr 2, 2022

@sahilph We just have to guess ;-) For example, if the incoming email to SL SMTP server looks like:

MAIL FROM: dummy@simplelogin.co
RCPT TO: user@example.com

Then now SL SMTP server can guess: "hmm the authenticated user has a recent interaction with user@example.com on alias alias1@simplelogin.co, so we should change the From to alias1@simplelogin.co". SMTP server then delivers the reply on behalf of alias1@simplelogin.co to user@example.com to example.com's SMTP server.

@sahilph
Copy link
Author

sahilph commented Apr 2, 2022

@developStorm I don't think we should rely on guesswork while sending emails.

Also what about sending to someone who we have never sent before ?

@developStorm
Copy link
Contributor

@sahilph Yeah guess is not a perfect idea, but I think it's worse to have user add every single alias they have, when they have let's say 50+ aliases and need to be able to reply as all those aliases.

For new recipients, user can manually add them on SL portal before they proceed, as what they have to do now to obtain reply address.

@sahilph
Copy link
Author

sahilph commented Apr 2, 2022

Yeah guess is not a perfect idea, but I think it's worse to have user add every single alias they have, when they have let's say 50+ aliases and need to be able to reply as all those aliases.

User don't have to add every single alias. They only need to add the ones they usually send from. For other aliases those who dont have SMTP enabled, sending via reverse-alias is still possible

For new recipients, user can manually add them on SL portal before they proceed, as what they have to do now to obtain reply address.

Then what's the point of SMTP? users can do that already do that using reverse-alias.

The whole point of SMTP access (and this PR) is that, if users want to send emails from the aliases they most often send from, there would be no need of creating reverse-alias. Users can just the emails using their aliases via SMTP using their favorite email client. No need to manually login to SL portal and create reverse aliases everytime.

For other non-frequently used aliases, use the reverse-alias method as that would still work.

TL;DR: For sending via most-frequently used alias -> Use SMTP ; otherwise -> use the regular-reverse alias method.

@sahilph
Copy link
Author

sahilph commented Apr 2, 2022

@developStorm
Check out #679 as to what I mean. I was facing similar issues.
Here are a few comments highlighting the issue:
#679 (reply in thread)
#679 (reply in thread)

Copy link
Contributor

@nguyenkims nguyenkims left a comment

Choose a reason for hiding this comment

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

The PR looks really good! I haven't run it though, it'd be great if you can include how to test this new service in the CONTRBUTING file.

I left some comments, lots of them are rather nits though.

README.md Show resolved Hide resolved
README.md Show resolved Hide resolved
app/api/views/alias.py Outdated Show resolved Hide resolved
app/api/views/alias.py Show resolved Hide resolved
app/config.py Outdated
@@ -444,3 +444,7 @@ def setup_nameservers():


NAMESERVERS = setup_nameservers()

# For SMTP support
SMTP_SSL_CERT_FILE = os.environ.get("SMTP_SSL_CERT_FILE") or "/smtp_ssl_cert.pem"
Copy link
Contributor

Choose a reason for hiding this comment

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

Can we add PATH suffix to be explicit, so SMTP_SSL_CERT_FILEPATH instead of SMTP_SSL_CERT_FILE?

Copy link
Contributor

Choose a reason for hiding this comment

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

And as existing installations don't have the certificates and keys file, should this new service disabled unless the env variables are set?

Copy link
Author

Choose a reason for hiding this comment

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

Can we add PATH suffix to be explicit, so SMTP_SSL_CERT_FILEPATH instead of SMTP_SSL_CERT_FILE?

I have changed this.

Copy link
Author

Choose a reason for hiding this comment

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

And as existing installations don't have the certificates and keys file, should this new service disabled unless the env variables are set?

The service won't start at all, if the files cert/key files do not exist and thus would remain disabled.

SMTP_handler.py Outdated Show resolved Hide resolved
return [(False, status.E515)]

user = alias.user
mailbox = Mailbox.get_by(id=alias.mailbox_id)
Copy link
Contributor

Choose a reason for hiding this comment

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

the mailbox should come from the envelope.mail_from as an alias can have several mailboxes.

Copy link
Author

Choose a reason for hiding this comment

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

Well, if user can have multiple inboxes, it won't be relevant to include mailbox here.
I added it only for logging as I thought Email Log requires it. But it doesn't, so it can be removed. What are your thoughts. ?

Copy link
Contributor

Choose a reason for hiding this comment

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

The mailbox_id is needed by EmailLog. In this reply phase, this is the mailbox (or a mailbox's authorized address) that sends the email.

Copy link
Author

Choose a reason for hiding this comment

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

But for SMTP phase, the envelope.mail_from, will be the alias itself.

So how to get the relevant mailbox in case of multiple mailboxes ?

I think it just just be removed as we are already setting is_SMTP=True

SMTP_handler.py Outdated Show resolved Hide resolved
SMTP_handler.py Outdated Show resolved Hide resolved
SMTP_handler.py Outdated Show resolved Hide resolved
Copy link
Contributor

@nguyenkims nguyenkims left a comment

Choose a reason for hiding this comment

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

I just answered some questions.

README.md Show resolved Hide resolved
app/api/views/alias.py Show resolved Hide resolved
app/models.py Show resolved Hide resolved
email_handler.py Outdated
add_or_replace_header(msg, "Reply-To", new_reply_to_header)
LOG.d("Reply-To header, new:%s, old:%s", new_reply_to_header, reply_to_header)
# Don't Change Headers when SMTP is enabled for alias
if not is_SMTP_enabled_for_alias:
Copy link
Contributor

Choose a reason for hiding this comment

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

I did not get this part. Can you please explain this ?

Currently SMTP handler receives an email from an alias to a contact and send the email to the contact.

What I meant is in addition to that flow, SMTP handler can also handle the case user sends an email from an alias to a reverse alias. So when SMTP handler receives an email from an alias to a reverse alias, it extracts the contact address from the reverse alias and continue the normal process.

Copy link
Author

@sahilph sahilph left a comment

Choose a reason for hiding this comment

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

@nguyenkims I have replied to some of the comments.

app/api/views/alias.py Show resolved Hide resolved
README.md Show resolved Hide resolved
app/models.py Show resolved Hide resolved
email_handler.py Outdated
add_or_replace_header(msg, "Reply-To", new_reply_to_header)
LOG.d("Reply-To header, new:%s, old:%s", new_reply_to_header, reply_to_header)
# Don't Change Headers when SMTP is enabled for alias
if not is_SMTP_enabled_for_alias:
Copy link
Author

Choose a reason for hiding this comment

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

Oh alright, now I kinda get it.

Say a user is trying to send via SMTP, but instead of sending to the actual contact address, they send to the contact's reverse alias.

So you are saying that, the SMTP handler should handle this case as well, right?

Also, in this scenario, the reverse-alias, should belong to the alias, correct ?

Copy link
Contributor

@nguyenkims nguyenkims left a comment

Choose a reason for hiding this comment

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

Hey just left a thought. I think we can merge the PR and it can be used as-is for the self hosting. For SimpleLogin.io version, I think we need to figure out a way to have Postfix protecting this new service.

README.md Show resolved Hide resolved
@sahilph
Copy link
Author

sahilph commented Apr 13, 2022

I think we can merge the PR and it can be used as-is for the self hosting.

Oh that's nice.

For SimpleLogin.io version, I think we need to figure out a way to have Postfix protecting this new service.

Yes, if we could figure out a way to make postfix talk to python, that would be great as that is the only thing preventing this from happening.

@sahilph
Copy link
Author

sahilph commented Apr 13, 2022

Also, I have replied to the comments.

Copy link
Contributor

@nguyenkims nguyenkims left a comment

Choose a reason for hiding this comment

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

I just left 2 comments.

app/models.py Outdated
@@ -2557,6 +2575,45 @@ class AliasMailbox(Base, ModelMixin):
alias = orm.relationship(Alias)


class SMTPCredentials(Base, ModelMixin, PasswordOracle):
__tablename__ = "SMTP_credentials"
__table_args__ = (sa.UniqueConstraint("alias_id", name="uq_alias"),)
Copy link
Contributor

Choose a reason for hiding this comment

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

@sahilph re in case you haven't seen this comment.

email_handler.py Outdated
add_or_replace_header(msg, "Reply-To", new_reply_to_header)
LOG.d("Reply-To header, new:%s, old:%s", new_reply_to_header, reply_to_header)
# Don't Change Headers when SMTP is enabled for alias
if not is_SMTP_enabled_for_alias:
Copy link
Contributor

Choose a reason for hiding this comment

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

As I mentioned earlier, the email will fail dmarc alignement so we can't keep the original header. We cannot keep original header at the moment. This code part needs to be reverted in order for the PR to be merged.

ARC is another topic that should probably be addressed in another PR.

Copy link
Contributor

@nguyenkims nguyenkims left a comment

Choose a reason for hiding this comment

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

I realized that I haven't done the follow up on the discussion on SMTP_handler file.

SMTP_handler.py Outdated

contact = Contact.get_by(website_email=website_email)
if not contact:
LOG.w(f"No contact with {website_email} as website email")
Copy link
Contributor

Choose a reason for hiding this comment

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

What I meant is this is a rather normal case so we shouldn't use logging warning level here. info or debug level is more suitable.

SMTP_handler.py Outdated
LOG.w(f"No contact with {website_email} as website email")
try:
LOG.d("Create or get contact for website_email:%s", website_email)
contact = get_or_create_contact("", website_email, alias, msg)
Copy link
Contributor

Choose a reason for hiding this comment

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

I mean the current get_or_create_contact handles a lot of cases that aren't necessary for this usage. I think it's better to have a smaller method, which is a stripped down version of the get_or_create_contact to be used here.

return [(False, status.E515)]

user = alias.user
mailbox = Mailbox.get_by(id=alias.mailbox_id)
Copy link
Contributor

Choose a reason for hiding this comment

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

The mailbox_id is needed by EmailLog. In this reply phase, this is the mailbox (or a mailbox's authorized address) that sends the email.

LOG.e("%s domain isn't known", alias)
return False, status.E503

contact = Contact.get_by(website_email=website_email)
Copy link
Contributor

Choose a reason for hiding this comment

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

As mentioned in another comment, I think we can also handle the case where website_email is a reverse alias here. This should be relatively easy IMO.

Copy link
Author

Choose a reason for hiding this comment

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

I have added some code to handle this case.

Copy link
Author

@sahilph sahilph left a comment

Choose a reason for hiding this comment

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

@nguyenkims I have made some changes and left some comments.

LOG.e("%s domain isn't known", alias)
return False, status.E503

contact = Contact.get_by(website_email=website_email)
Copy link
Author

Choose a reason for hiding this comment

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

I have added some code to handle this case.

SMTP_handler.py Outdated

contact = Contact.get_by(website_email=website_email)
if not contact:
LOG.w(f"No contact with {website_email} as website email")
Copy link
Author

Choose a reason for hiding this comment

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

I have changed this to debug level.

SMTP_handler.py Outdated
LOG.w(f"No contact with {website_email} as website email")
try:
LOG.d("Create or get contact for website_email:%s", website_email)
contact = get_or_create_contact("", website_email, alias, msg)
Copy link
Author

Choose a reason for hiding this comment

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

I have created a similar stripped down version get_or_create_contact_for_SMTP_phase please check if its fine.

return [(False, status.E515)]

user = alias.user
mailbox = Mailbox.get_by(id=alias.mailbox_id)
Copy link
Author

Choose a reason for hiding this comment

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

But for SMTP phase, the envelope.mail_from, will be the alias itself.

So how to get the relevant mailbox in case of multiple mailboxes ?

I think it just just be removed as we are already setting is_SMTP=True

app/models.py Outdated
@@ -2557,6 +2575,45 @@ class AliasMailbox(Base, ModelMixin):
alias = orm.relationship(Alias)


class SMTPCredentials(Base, ModelMixin, PasswordOracle):
__tablename__ = "SMTP_credentials"
__table_args__ = (sa.UniqueConstraint("alias_id", name="uq_alias"),)
Copy link
Author

Choose a reason for hiding this comment

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

I have removed the __table_args__

email_handler.py Outdated
add_or_replace_header(msg, "Reply-To", new_reply_to_header)
LOG.d("Reply-To header, new:%s, old:%s", new_reply_to_header, reply_to_header)
# Don't Change Headers when SMTP is enabled for alias
if not is_SMTP_enabled_for_alias:
Copy link
Author

Choose a reason for hiding this comment

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

I have reverted this part.

else:
SMTPCredentials.delete_by_alias_id(alias_id=alias.id)
alias.enable_SMTP = enable_SMTP
changed = True
Copy link
Collaborator

Choose a reason for hiding this comment

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

Instead of generating the password in the client it'd be better to generate it server-side and show it once to the client on successful activation:

  • No need to add extra dependencies in python and in js (ie. nanoid)
  • No need to have js code in the front
  • No strange 21 character validation
  • Other clients have no need to add nanoid dependencies.

Just generate a random secret, store it in the db and in the next page display show the credentials warning the user that the credentials won't be able to be displayed any more times.

Copy link
Author

Choose a reason for hiding this comment

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

@nguyenkims Is this really needed at this stage ?

Copy link
Contributor

Choose a reason for hiding this comment

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

@sahilph generally I think it's better to generate secret on server as we can't control client environment. Adding this change should be quite simple right as the server generates the password and send this back to the client to display?

@acasajus
Copy link
Collaborator

Hey, nice PR. But as a general comment, there aren't any tests added. I would suggest to add some:

  • Creating the SMTPCredentials does what it's supposed to and that passwords can be validated
  • The SMTP handler is able to receive connections, authenticate/fail, and deliver the messages to where it's expected.

@sahilph
Copy link
Author

sahilph commented Apr 24, 2022

@acasajus Thanks for the feedback.
Yes, I understand that there are no tests, and I can certainly write some if needed, but I believe, ideally, tests should be written by someone else, because if I also write tests for this, it will be highly biased. (I certainly don't want my one code to break now do I ? 😁 )

@nguyenkims
Copy link
Contributor

@sahilph @acasajus Having tests for every PR is indeed a good idea. We actually set a limit in terms of code coverage under which a PR won't pass the CI. And tests should be written by the PR author :). It happens several times that writing tests help discover cases that we don't realize during the development. We are not extreme in terms of test coverage though but there should be enough tests to cover the most frequent cases.

@sahilph sahilph requested a review from cquintana92 as a code owner May 10, 2022 09:25
@Robin-Sch
Copy link

any update? I really need this asap

@EvilAaron
Copy link

Yeah, I am following this with great interest as well.

I purchased a year of SL and love much about the service and have brought 2 of my domain on board.

After using for a couple of weeks though and mucking with the reverse alias system and clogging my contacts up with extra redundant emails in their profiles, this is the one part that feels clunky.

Really hoping this is solved with this above proposed solution. If no way to "send as" SL aliases from email client arrives in a year when renewal arrives, I would have to consider pulling my domains and finding a service for email support and relegate SL to just throwaway emails on their domains.

I am hoping a way will be found, as I love SL so far and would love for it to fit.

@c0nfigurati0n
Copy link

c0nfigurati0n commented Aug 4, 2022

I'm not sure i get the point of this function, seen as SimpleLogin is indeed not meant to be a transactional email service. But if there are other use cases for this function, then i would say, "why not".

@nephalemsec
Copy link

What exactly is holding this back? This has been sitting in a completed state for so long, how can we get it over the line?

@edsimpsons83
Copy link

Agreed, disappointing to see such a valuable PR just sitting here.

@nguyenkims
Copy link
Contributor

nguyenkims commented Oct 2, 2022

@nephalemsec @edsimpsons83 the PR isn't complete. We asked for adding tests into it (among other things) and haven't received any news yet. And please keep in mind that this PR is rather for self hosting. To make it available on SL.io, we need to adapt the infra and work on an anti-abuse system first.

@nephalemsec
Copy link

@nephalemsec @edsimpsons83 the PR isn't complete. We asked for adding tests into it (among other things) and haven't received any news yet. And please keep in mind that this PR is rather for self hosting. To make it available on SL.io, we need to adapt the infra and work on an anti-abuse system first.

Understood. As @sahilph has stated he believes there would be a bias with the PR author making unit tests is there a plan for SL team to assist?

@nguyenkims
Copy link
Contributor

@nephalemsec providing unit tests should actually be part of the PR and it is up to the author to write tests.

@c0nfigurati0n
Copy link

I'm not sure i get the point of this function, seen as SimpleLogin is indeed not meant to be a transactional email service. But if there are other use cases for this function, then i would say, "why not".

Sadly i didn't get a reply from someone explaining the purpose of this function to me. :(

@sugarfunk
Copy link

I'm not sure i get the point of this function, seen as SimpleLogin is indeed not meant to be a transactional email service. But if there are other use cases for this function, then i would say, "why not".

Sadly i didn't get a reply from someone explaining the purpose of this function to me. :(

To be able to send from an alias without needing to log into the SL interface comes to mind.

@jasonhe54
Copy link

So, is this PR just dead now after 6 months? Is there any intent on supporting this feature? Would be nice to actually be able to use SL as a way to send emails from an alias similar to the way iCloud Plus currently lets you do it.

@Haulien
Copy link

Haulien commented Nov 9, 2023

It's a shame this PR has been left to die. Time to look for a better alternative to SL.

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.

None yet