def test_no_smtp() -> None: # pragma: no cover with pytest.raises(ServiceUnavailable): connector.get_instance() log.warning("Skipping {} tests: service not available", CONNECTOR) return None
def send_notification( subject: str, template: str, # if None will be sent to the administrator to_address: Optional[str] = None, data: Optional[Dict[str, Any]] = None, user: Optional[User] = None, send_async: bool = False, ) -> bool: # Always enabled during tests if not Connector.check_availability("smtp"): # pragma: no cover return False title = get_project_configuration("project.title", default="Unkown title") reply_to = Env.get("SMTP_NOREPLY", Env.get("SMTP_ADMIN", "")) if data is None: data = {} data.setdefault("project", title) data.setdefault("reply_to", reply_to) if user: data.setdefault("username", user.email) data.setdefault("name", user.name) data.setdefault("surname", user.surname) html_body, plain_body = get_html_template(template, data) if not html_body: # pragma: no cover log.error("Can't load {}", template) return False subject = f"{title}: {subject}" if send_async: Mail.send_async( subject=subject, body=html_body, to_address=to_address, plain_body=plain_body, ) return False smtp_client = smtp.get_instance() return smtp_client.send( subject=subject, body=html_body, to_address=to_address, plain_body=plain_body, )
def post(self, **kwargs: Any) -> Response: """ Register new user """ email = kwargs.get("email") user = self.auth.get_user(username=email) if user is not None: raise Conflict(f"This user already exists: {email}") password_confirm = kwargs.pop("password_confirm") if kwargs.get("password") != password_confirm: raise Conflict("Your password doesn't match the confirmation") if self.auth.VERIFY_PASSWORD_STRENGTH: check, msg = self.auth.verify_password_strength( kwargs.get("password"), None) if not check: raise Conflict(msg) kwargs["is_active"] = False user = self.auth.create_user(kwargs, [self.auth.default_role]) default_group = self.auth.get_group(name=DEFAULT_GROUP_NAME) self.auth.add_user_to_group(user, default_group) self.auth.save_user(user) self.log_event(self.events.create, user, kwargs) try: smtp_client = smtp.get_instance() if Env.get_bool("REGISTRATION_NOTIFICATIONS"): # Sending an email to the administrator title = get_project_configuration("project.title", default="Unkown title") subject = f"{title} New credentials requested" body = f"New credentials request from {user.email}" smtp_client.send(body, subject) send_activation_link(smtp_client, self.auth, user) except BaseException as e: # pragma: no cover self.auth.delete_user(user) raise ServiceUnavailable( f"Errors during account registration: {e}") return self.response( "We are sending an email to your email address where " "you will find the link to activate your account")
def post(self, username: str) -> Response: self.auth.verify_blocked_username(username) user = self.auth.get_user(username=username) # if user is None this endpoint does nothing but the response # remain the same to prevent any user guessing if user is not None: smtp_client = smtp.get_instance() send_activation_link(smtp_client, self.auth, user) msg = ("We are sending an email to your email address where " "you will find the link to activate your account") return self.response(msg)
def post( self, user: User, subject: str, body: str, to: str, cc: Optional[List[str]] = None, bcc: Optional[List[str]] = None, dry_run: bool = False, ) -> Response: replaces: Dict[str, Any] = {} header_html = _get_html_template("email_header.html", replaces) footer_html = _get_html_template("email_footer.html", replaces) body = body.replace("\n", "<br/>") html_body = f"{header_html}{body}{footer_html}" plain_body = convert_html2text(html_body) if dry_run: return self.response( { "html_body": html_body, "plain_body": plain_body, "subject": subject, "to": to, "cc": cc, "bcc": bcc, } ) smtp_client = smtp.get_instance() smtp_client.send( body=html_body, subject=subject, to_address=to, from_address=None, cc=cc, bcc=bcc, plain_body=plain_body, ) return self.empty_response()
def post(self, reset_email: str) -> Response: reset_email = reset_email.lower() self.auth.verify_blocked_username(reset_email) user = self.auth.get_user(username=reset_email) if user is None: raise Forbidden( f"Sorry, {reset_email} is not recognized as a valid username", ) self.auth.verify_user_status(user) title = get_project_configuration("project.title", default="Unkown title") reset_token, payload = self.auth.create_temporary_token( user, self.auth.PWD_RESET) server_url = get_frontend_url() rt = reset_token.replace(".", "+") uri = Env.get("RESET_PASSWORD_URI", "/public/reset") complete_uri = f"{server_url}{uri}/{rt}" smtp_client = smtp.get_instance() send_password_reset_link(smtp_client, complete_uri, title, reset_email) ################## # Completing the reset task self.auth.save_token(user, reset_token, payload, token_type=self.auth.PWD_RESET) msg = "We'll send instructions to the email provided if it's associated " msg += "with an account. Please check your spam/junk folder." self.log_event(self.events.reset_password_request, user=user) return self.response(msg)
def post(self, **kwargs: Any) -> Response: roles: List[str] = kwargs.pop("roles", []) payload = kwargs.copy() group_id = kwargs.pop("group") email_notification = kwargs.pop("email_notification", False) unhashed_password = kwargs["password"] # If created by admins users must accept privacy at first login kwargs["privacy_accepted"] = False try: user = self.auth.create_user(kwargs, roles) self.auth.save_user(user) except DatabaseDuplicatedEntry as e: if Connector.authentication_service == "sqlalchemy": self.auth.db.session.rollback() raise Conflict(str(e)) group = self.auth.get_group(group_id=group_id) if not group: # Can't be reached because grup_id is prefiltered by marshmallow raise NotFound("This group cannot be found") # pragma: no cover self.auth.add_user_to_group(user, group) if email_notification and unhashed_password is not None: smtp_client = smtp.get_instance() send_notification(smtp_client, user, unhashed_password, is_update=False) self.log_event(self.events.create, user, payload) return self.response(user.uuid)
def wrapper(self, *args, **kwargs): try: return func(self, *args, **kwargs) except BaseException: task_id = self.request.id task_name = self.request.task log.error("Celery task {} failed ({})", task_id, task_name) arguments = str(self.request.args) log.error("Failed task arguments: {}", arguments[0:256]) log.error("Task error: {}", traceback.format_exc()) if Connector.check_availability("smtp"): log.info("Sending error report by email", task_id, task_name) body = f""" Celery task {task_id} failed Name: {task_name} Arguments: {self.request.args} Error: {traceback.format_exc()} """ project = get_project_configuration( "project.title", default="Unkown title", ) subject = f"{project}: task {task_name} failed" from restapi.connectors import smtp smtp_client = smtp.get_instance() smtp_client.send(body, subject)
def test_smtp(app: Flask, faker: Faker) -> None: obj = connector.get_instance() assert obj is not None assert obj.smtp is not None obj = connector.get_instance(port="465") assert obj is not None assert obj.smtp is not None obj = connector.get_instance(port="587") assert obj is not None assert obj.smtp is not None assert obj.send("body", "subject") assert obj.send("body", "subject", "to_addr") assert obj.send("body", "subject", "to_addr", "from_addr") obj = connector.get_instance() mail = BaseTests.read_mock_email() body = mail.get("body") headers = mail.get("headers") assert body is not None assert headers is not None # Subject: is a key in the MIMEText assert "Subject: subject" in headers assert mail.get("from") == "from_addr" assert mail.get("cc") == ["to_addr"] assert mail.get("bcc") is None assert obj.send("body", "subject", "to_addr", "from_addr", cc="test1", bcc="test2") mail = BaseTests.read_mock_email() body = mail.get("body") headers = mail.get("headers") assert body is not None assert headers is not None # Subject: is a key in the MIMEText assert "Subject: subject" in headers assert mail.get("from") == "from_addr" # format is [to, [cc...], [bcc...]] assert mail.get("cc") == ["to_addr", ["test1"], ["test2"]] assert obj.send( "body", "subject", "to_addr", "from_addr", cc=["test1", "test2"], bcc=["test3", "test4"], ) mail = BaseTests.read_mock_email() body = mail.get("body") headers = mail.get("headers") assert body is not None assert headers is not None # Subject: is a key in the MIMEText assert "Subject: subject" in headers assert mail.get("from") == "from_addr" # format is [to, [cc...], [bcc...]] assert mail.get("cc") == ["to_addr", ["test1", "test2"], ["test3", "test4"]] # This is a special from_address, used to raise SMTPException assert not obj.send("body", "subject", "to_addr", "invalid1") # This is a special from_address, used to raise Exception obj = connector.get_instance() assert not obj.send("body", "subject", "to_addr", "invalid2") # This is NOT a special from_address obj = connector.get_instance() assert obj.send("body", "subject", "to_addr", "invalid3") # Test that cc and bcc with wrong types are ignored assert obj.send( "body", "subject", "to_addr", "from_addr", cc=10, # type: ignore bcc=20, # type: ignore ) mail = BaseTests.read_mock_email() body = mail.get("body") headers = mail.get("headers") assert body is not None assert headers is not None # Subject: is a key in the MIMEText assert "Subject: subject" in headers # cc and bcc with wrong type (int in this case!) are ignored assert mail.get("from") == "from_addr" # format is [to, [cc...], [bcc...]] assert mail.get("cc") == ["to_addr"] with connector.get_instance() as obj: assert obj is not None assert obj.smtp is not None # assert obj.smtp is None with connector.get_instance(noreply="", admin="") as obj: assert not obj.send("body", "subject") assert not obj.send("body", "subject", "to_addr") assert obj.send("body", "subject", "to_addr", "from_addr") obj = connector.get_instance() assert obj.is_connected() obj.disconnect() # a second disconnect should not raise any error obj.disconnect() assert not obj.is_connected()
def put(self, user_id: str, **kwargs: Any) -> Response: user = self.auth.get_user(user_id=user_id) if user is None: raise NotFound( "This user cannot be found or you are not authorized") if "password" in kwargs: unhashed_password = kwargs["password"] kwargs["password"] = BaseAuthentication.get_password_hash( kwargs["password"]) else: unhashed_password = None payload = kwargs.copy() roles: List[str] = kwargs.pop("roles", []) group_id = kwargs.pop("group", None) email_notification = kwargs.pop("email_notification", False) self.auth.link_roles(user, roles) userdata, extra_userdata = self.auth.custom_user_properties_pre(kwargs) prev_expiration = user.expiration self.auth.db.update_properties(user, userdata) self.auth.custom_user_properties_post(user, userdata, extra_userdata, self.auth.db) self.auth.save_user(user) if group_id is not None: group = self.auth.get_group(group_id=group_id) if not group: # Can't be reached because grup_id is prefiltered by marshmallow raise NotFound( "This group cannot be found") # pragma: no cover self.auth.add_user_to_group(user, group) if email_notification and unhashed_password is not None: smtp_client = smtp.get_instance() send_notification(smtp_client, user, unhashed_password, is_update=True) if user.expiration: # Set expiration on a previously non-expiring account # or update the expiration by reducing the validity period # In both cases tokens should be invalited to prevent to have tokens # with TTL > account validity # dt_lower (alias for date_lower_than) is a comparison fn that ignores tz if prev_expiration is None or dt_lower(user.expiration, prev_expiration): for token in self.auth.get_tokens(user=user): # Invalidate all tokens with expiration after the account expiration if dt_lower(user.expiration, token["expiration"]): self.auth.invalidate_token(token=token["token"]) self.log_event(self.events.modify, user, payload) return self.empty_response()
def test_smtp(app: Flask, faker: Faker) -> None: # mailmock is always enabled during core tests if not Connector.check_availability(CONNECTOR): # pragma: no cover try: obj = connector.get_instance() pytest.fail("No exception raised") # pragma: no cover except ServiceUnavailable: pass log.warning("Skipping {} tests: service not available", CONNECTOR) return None # try: # connector.get_instance(host="invalidhostname", port=123) # pytest.fail("No exception raised on unavailable service") # pragma: no cover # except ServiceUnavailable: # pass obj = connector.get_instance() assert obj is not None assert obj.smtp is not None obj = connector.get_instance(port=465) assert obj is not None assert obj.smtp is not None obj = connector.get_instance(port=587) assert obj is not None assert obj.smtp is not None assert obj.send("body", "subject") assert obj.send("body", "subject", "to_addr") assert obj.send("body", "subject", "to_addr", "from_addr") obj = connector.get_instance() mail = BaseTests.read_mock_email() body = mail.get("body") headers = mail.get("headers") assert body is not None assert headers is not None # Subject: is a key in the MIMEText assert "Subject: subject" in headers assert mail.get("from") == "from_addr" assert mail.get("cc") == ["to_addr"] assert mail.get("bcc") is None assert obj.send("body", "subject", "to_addr", "from_addr", cc="test1", bcc="test2") mail = BaseTests.read_mock_email() body = mail.get("body") headers = mail.get("headers") assert body is not None assert headers is not None # Subject: is a key in the MIMEText assert "Subject: subject" in headers assert mail.get("from") == "from_addr" # format is [to, [cc...], [bcc...]] assert mail.get("cc") == ["to_addr", ["test1"], ["test2"]] assert obj.send( "body", "subject", "to_addr", "from_addr", cc=["test1", "test2"], bcc=["test3", "test4"], ) mail = BaseTests.read_mock_email() body = mail.get("body") headers = mail.get("headers") assert body is not None assert headers is not None # Subject: is a key in the MIMEText assert "Subject: subject" in headers assert mail.get("from") == "from_addr" # format is [to, [cc...], [bcc...]] assert mail.get("cc") == [ "to_addr", ["test1", "test2"], ["test3", "test4"] ] # This is a special from_address, used to raise SMTPException assert not obj.send("body", "subject", "to_addr", "invalid1") # This is a special from_address, used to raise BaseException assert not obj.send("body", "subject", "to_addr", "invalid2") # This is NOT a special from_address assert obj.send("body", "subject", "to_addr", "invalid3") assert obj.send("body", "subject", "to_addr", "from_addr", cc=10, bcc=20) mail = BaseTests.read_mock_email() body = mail.get("body") headers = mail.get("headers") assert body is not None assert headers is not None # Subject: is a key in the MIMEText assert "Subject: subject" in headers # cc and bcc with wrong type (int in this case!) are ignored assert mail.get("from") == "from_addr" # format is [to, [cc...], [bcc...]] assert mail.get("cc") == ["to_addr"] with connector.get_instance() as obj: assert obj is not None assert obj.smtp is not None # assert obj.smtp is None with connector.get_instance(noreply=None, admin=None) as obj: assert not obj.send("body", "subject") assert not obj.send("body", "subject", "to_addr") assert obj.send("body", "subject", "to_addr", "from_addr") obj = connector.get_instance() assert obj.is_connected() obj.disconnect() # a second disconnect should not raise any error obj.disconnect() assert not obj.is_connected()