def _create_content_event(self, operation: OperationType, content: Content, context: TracimContext) -> None: current_user = context.safe_current_user() content_api = ContentApi(context.dbsession, current_user, self._config) content_in_context = content_api.get_content_in_context(content) content_schema = EventApi.get_content_schema_for_type(content.type) content_dict = content_schema.dump(content_in_context).data workspace_api = WorkspaceApi(context.dbsession, current_user, self._config, show_deleted=True) workspace_in_context = workspace_api.get_workspace_with_context( workspace_api.get_one(content_in_context.workspace.workspace_id)) fields = { Event.CONTENT_FIELD: content_dict, Event.WORKSPACE_FIELD: EventApi.workspace_schema.dump(workspace_in_context).data, } event_api = EventApi(current_user, context.dbsession, self._config) event_api.create_event( entity_type=EntityType.CONTENT, operation=operation, additional_fields=fields, entity_subtype=content.type, context=context, )
def create_workspace(self, context, request: TracimRequest, hapic_data=None): """ Create a workspace. This route is for trusted users and administrators. """ app_config = request.registry.settings["CFG"] # type: CFG wapi = WorkspaceApi( current_user=request.current_user, session=request.dbsession, config=app_config # User ) parent = None if hapic_data.body.parent_id: parent = wapi.get_one(workspace_id=hapic_data.body.parent_id) workspace = wapi.create_workspace( label=hapic_data.body.label, description=hapic_data.body.description, access_type=hapic_data.body.access_type, save_now=True, agenda_enabled=hapic_data.body.agenda_enabled, public_download_enabled=hapic_data.body.public_download_enabled, public_upload_enabled=hapic_data.body.public_upload_enabled, default_user_role=hapic_data.body.default_user_role, parent=parent, ) wapi.execute_created_workspace_actions(workspace) return wapi.get_workspace_with_context(workspace)
def _create_subscription_event(self, operation: OperationType, subscription: WorkspaceSubscription, context: TracimContext) -> None: current_user = context.safe_current_user() workspace_api = WorkspaceApi( session=context.dbsession, config=self._config, current_user=None, ) workspace_in_context = workspace_api.get_workspace_with_context( workspace_api.get_one(subscription.workspace_id)) user_api = UserApi(current_user, context.dbsession, self._config, show_deleted=True) subscription_author_in_context = user_api.get_user_with_context( subscription.author) fields = { Event.WORKSPACE_FIELD: EventApi.workspace_schema.dump(workspace_in_context).data, Event.SUBSCRIPTION_FIELD: EventApi.workspace_subscription_schema.dump(subscription).data, Event.USER_FIELD: EventApi.user_schema.dump(subscription_author_in_context).data, } event_api = EventApi(current_user, context.dbsession, self._config) event_api.create_event( entity_type=EntityType.WORKSPACE_SUBSCRIPTION, operation=operation, additional_fields=fields, context=context, )
def update_workspace(self, context, request: TracimRequest, hapic_data=None): """ Update a workspace. This route is for trusted users and administrators. Note : a trusted user can only update spaces on which he/she is space manager """ app_config = request.registry.settings["CFG"] # type: CFG wapi = WorkspaceApi( current_user=request.current_user, session=request.dbsession, config=app_config # User ) wapi.update_workspace( request.current_workspace, label=hapic_data.body.label, description=hapic_data.body.description, agenda_enabled=hapic_data.body.agenda_enabled, public_download_enabled=hapic_data.body.public_download_enabled, public_upload_enabled=hapic_data.body.public_upload_enabled, default_user_role=hapic_data.body.default_user_role, save_now=True, ) wapi.execute_update_workspace_actions(request.current_workspace) return wapi.get_workspace_with_context(request.current_workspace)
def workspace(self, context, request: TracimRequest, hapic_data=None): """ Get workspace informations """ app_config = request.registry.settings['CFG'] wapi = WorkspaceApi( current_user=request.current_user, # User session=request.dbsession, config=app_config, ) return wapi.get_workspace_with_context(request.current_workspace)
def workspace(self, context, request: TracimRequest, hapic_data=None): """ Get workspace informations """ app_config = request.registry.settings['CFG'] wapi = WorkspaceApi( current_user=request.current_user, # User session=request.dbsession, config=app_config, ) return wapi.get_workspace_with_context(request.current_workspace)
def workspace_disk_space(self, context, request: TracimRequest, hapic_data=None): """ Get workspace space info """ app_config = request.registry.settings["CFG"] # type: CFG wapi = WorkspaceApi( current_user=request.current_user, session=request.dbsession, config=app_config # User ) return wapi.get_workspace_with_context(request.current_workspace)
def join_workspace(self, context, request: TracimRequest, hapic_data=None): """ Join a workspace. Only possible for OPEN workspaces. Subscribing to a ON_REQUEST workspace is done through /api/users/<user_id>/workspace_subscriptions. """ app_config = request.registry.settings["CFG"] # type: CFG wapi = WorkspaceApi( current_user=request.candidate_user, # User session=request.dbsession, config=app_config, ) workspace = wapi.add_current_user_as_member(workspace_id=hapic_data.body["workspace_id"]) return wapi.get_workspace_with_context(workspace)
def get_accessible_workspaces( self, context, request: TracimRequest, hapic_data: HapicData ) -> typing.List[WorkspaceInContext]: """ Return the list of accessible workspaces by the given user id. An accessible workspace is: - a workspace the user is not member of (`workspaces` API returns them) - has an OPEN or ON_REQUEST access type """ app_config = request.registry.settings["CFG"] # type: CFG wapi = WorkspaceApi( current_user=request.candidate_user, session=request.dbsession, config=app_config, ) workspaces = wapi.get_all_accessible_by_user(request.candidate_user) return [wapi.get_workspace_with_context(workspace) for workspace in workspaces]
def create_workspace(self, context, request: TracimRequest, hapic_data=None): # nopep8 """ Create a workspace. This route is for trusted users and administrators. """ app_config = request.registry.settings['CFG'] wapi = WorkspaceApi( current_user=request.current_user, # User session=request.dbsession, config=app_config, ) workspace = wapi.create_workspace( label=hapic_data.body.label, description=hapic_data.body.description, save_now=True, ) return wapi.get_workspace_with_context(workspace)
def account_workspace(self, context, request: TracimRequest, hapic_data=None): """ Get list of auth user workspaces """ app_config = request.registry.settings['CFG'] wapi = WorkspaceApi( current_user=request.current_user, # User session=request.dbsession, config=app_config, ) workspaces = wapi.get_all_for_user(request.current_user) return [ wapi.get_workspace_with_context(workspace) for workspace in workspaces ]
def user_workspace(self, context, request: TracimRequest, hapic_data=None): """ Get list of user workspaces """ app_config = request.registry.settings['CFG'] wapi = WorkspaceApi( current_user=request.candidate_user, # User session=request.dbsession, config=app_config, ) workspaces = wapi.get_all_for_user(request.candidate_user) return [ wapi.get_workspace_with_context(workspace) for workspace in workspaces ]
def _create_role_event(self, operation: OperationType, role: UserRoleInWorkspace, context: TracimContext) -> None: current_user = context.safe_current_user() workspace_api = WorkspaceApi( session=context.dbsession, config=self._config, show_deleted=True, # INFO - G.M - 2020-17-09 - we do explicitly don't set user here to not # have filter on workspace, in some case we do want user create event on workspace # he doesn't have access: when he remove itself from workspace for example current_user=None, ) workspace_in_context = workspace_api.get_workspace_with_context( workspace_api.get_one(role.workspace_id)) user_api = UserApi(current_user, context.dbsession, self._config, show_deleted=True) role_api = RoleApi(current_user=current_user, session=context.dbsession, config=self._config) try: user_field = EventApi.user_schema.dump( user_api.get_user_with_context(user_api.get_one( role.user_id))).data except UserDoesNotExist: # It is possible to have an already deleted user when deleting his roles. user_field = None role_in_context = role_api.get_user_role_workspace_with_context(role) fields = { Event.USER_FIELD: user_field, Event.WORKSPACE_FIELD: EventApi.workspace_schema.dump(workspace_in_context).data, Event.MEMBER_FIELD: EventApi.workspace_user_role_schema.dump(role_in_context).data, } event_api = EventApi(current_user, context.dbsession, self._config) event_api.create_event( entity_type=EntityType.WORKSPACE_MEMBER, operation=operation, additional_fields=fields, context=context, )
def workspaces(self, context, request: TracimRequest, hapic_data=None): """ Returns the list of all workspaces. This route is for admin only. Standard users must use their own route: /api/v2/users/me/workspaces """ app_config = request.registry.settings['CFG'] wapi = WorkspaceApi( current_user=request.current_user, # User session=request.dbsession, config=app_config, ) workspaces = wapi.get_all() return [ wapi.get_workspace_with_context(workspace) for workspace in workspaces ]
def workspaces(self, context, request: TracimRequest, hapic_data=None): """ Returns the list of all workspaces. This route is for admin only. Standard users must use their own route: /api/v2/users/me/workspaces """ app_config = request.registry.settings['CFG'] wapi = WorkspaceApi( current_user=request.current_user, # User session=request.dbsession, config=app_config, ) workspaces = wapi.get_all() return [ wapi.get_workspace_with_context(workspace) for workspace in workspaces ]
def user_workspace(self, context, request: TracimRequest, hapic_data=None): """ Get list of user workspaces """ app_config = request.registry.settings["CFG"] # type: CFG wapi = WorkspaceApi( current_user=request.candidate_user, # User session=request.dbsession, config=app_config, ) workspaces = wapi.get_all_for_user( request.candidate_user, include_owned=hapic_data.query.show_owned_workspace, include_with_role=hapic_data.query.show_workspace_with_role, parents_ids=hapic_data.query.parent_ids, ) return [wapi.get_workspace_with_context(workspace) for workspace in workspaces]
def update_workspace(self, context, request: TracimRequest, hapic_data=None): # nopep8 """ Update a workspace. This route is for trusted users and administrators. Note : a trusted user can only update spaces on which he/she is space manager """ app_config = request.registry.settings['CFG'] wapi = WorkspaceApi( current_user=request.current_user, # User session=request.dbsession, config=app_config, ) wapi.update_workspace( request.current_workspace, label=hapic_data.body.label, description=hapic_data.body.description, save_now=True, ) return wapi.get_workspace_with_context(request.current_workspace)
def account_workspace(self, context, request: TracimRequest, hapic_data=None): """ Get list of auth user workspaces """ app_config = request.registry.settings["CFG"] # type: CFG wapi = WorkspaceApi( current_user=request.current_user, session=request.dbsession, config=app_config # User ) workspaces = wapi.get_all_for_user(request.current_user) return [ wapi.get_workspace_with_context(workspace) for workspace in workspaces ]
def _create_workspace_event(self, operation: OperationType, workspace: Workspace, context: TracimContext) -> None: current_user = context.safe_current_user() api = WorkspaceApi(current_user=current_user, session=context.dbsession, config=self._config) workspace_in_context = api.get_workspace_with_context(workspace) fields = { Event.WORKSPACE_FIELD: EventApi.workspace_schema.dump(workspace_in_context).data } event_api = EventApi(current_user, context.dbsession, self._config) event_api.create_event( entity_type=EntityType.WORKSPACE, operation=operation, additional_fields=fields, context=context, )
def _create_mention_events( cls, mentions: typing.Iterable[Mention], content: Content, context: TracimContext ) -> None: role_api = RoleApi(session=context.dbsession, config=context.app_config, current_user=None) workspace_members_usernames = [ user.username for user in role_api.get_workspace_members(content.workspace_id) ] for mention in mentions: recipient = mention.recipient if ( recipient not in workspace_members_usernames and recipient not in ALL__GROUP_MENTIONS ): raise UserNotMemberOfWorkspace( "This user is not a member of this workspace: {}".format(mention.recipient) ) current_user = context.safe_current_user() content_api = ContentApi(context.dbsession, current_user, context.app_config) content_in_context = content_api.get_content_in_context(content) workspace_api = WorkspaceApi(context.dbsession, current_user, context.app_config) workspace_in_context = workspace_api.get_workspace_with_context( workspace_api.get_one(content_in_context.workspace.workspace_id) ) content_schema = EventApi.get_content_schema_for_type(content.type) content_dict = content_schema.dump(content_in_context).data common_fields = { Event.CONTENT_FIELD: content_dict, Event.WORKSPACE_FIELD: EventApi.workspace_schema.dump(workspace_in_context).data, } event_api = EventApi(current_user, context.dbsession, context.app_config) for mention in mentions: fields = {cls.MENTION_FIELD: {"recipient": mention.recipient, "id": mention.id}} fields.update(common_fields) event_api.create_event( entity_type=EntityType.MENTION, operation=OperationType.CREATED, additional_fields=fields, context=context, )
def workspaces(self, context, request: TracimRequest, hapic_data=None): """ Returns the list of all workspaces. This route is for admin only. Standard users must use their own route: /api/users/me/workspaces Filtering by parent_ids is possible through this endpoint """ app_config = request.registry.settings["CFG"] # type: CFG wapi = WorkspaceApi( current_user=request.current_user, session=request.dbsession, config=app_config # User ) workspaces = wapi.get_all_children( parent_ids=hapic_data.query.parent_ids) return [ wapi.get_workspace_with_context(workspace) for workspace in workspaces ]
def create_workspace(self, context, request: TracimRequest, hapic_data=None): """ Create a workspace. This route is for trusted users and administrators. """ app_config = request.registry.settings["CFG"] # type: CFG wapi = WorkspaceApi( current_user=request.current_user, session=request.dbsession, config=app_config # User ) workspace = wapi.create_workspace( label=hapic_data.body.label, description=hapic_data.body.description, save_now=True, agenda_enabled=hapic_data.body.agenda_enabled, ) wapi.execute_created_workspace_actions(workspace) return wapi.get_workspace_with_context(workspace)
def workspace(self) -> WorkspaceInContext: workspace_api = WorkspaceApi(config=self.config, session=self.dbsession, current_user=None) return workspace_api.get_workspace_with_context(self.upload_permission.workspace)
def upload_files( self, upload_permission: UploadPermission, uploader_username: str, message: typing.Optional[str], files: typing.List[cgi.FieldStorage], do_notify: bool = False, ) -> typing.List[ContentInContext]: content_api = ContentApi( config=self._config, current_user=upload_permission.author, session=self._session, namespaces_filter=[ContentNamespaces.UPLOAD], ) translator = Translator(app_config=self._config) _ = translator.get_translation current_datetime = datetime.utcnow() folder_label = _( "Files uploaded by {username} on {date} at {time}").format( username=uploader_username, date=format_date(current_datetime, locale=translator.default_lang), time=format_time(current_datetime, locale=translator.default_lang), ) try: upload_folder = content_api.create( content_type_slug=content_type_list.Folder.slug, workspace=upload_permission.workspace, label=folder_label, do_notify=False, do_save=True, content_namespace=ContentNamespaces.UPLOAD, ) except ContentFilenameAlreadyUsedInFolder: upload_folder = content_api.get_one_by_filename( filename=folder_label, workspace=upload_permission.workspace) created_contents = [] if message: comment_message = _("Message from {username}: {message}").format( username=uploader_username, message=message) else: comment_message = _("Uploaded by {username}.").format( username=uploader_username) for _file in files: content = content_api.create( filename=_file.filename, content_type_slug=content_type_list.File.slug, workspace=upload_permission.workspace, parent=upload_folder, do_notify=False, content_namespace=ContentNamespaces.UPLOAD, ) content_api.save(content, ActionDescription.CREATION) with new_revision(session=self._session, tm=transaction.manager, content=content): content_api.update_file_data( content, new_filename=_file.filename, new_mimetype=_file.type, new_content=_file.file, ) content_api.create_comment(parent=content, content=comment_message, do_save=True, do_notify=False) created_contents.append( content_api.get_content_in_context(content)) content_api.execute_created_content_actions(content) if do_notify: workspace_lib = WorkspaceApi(config=self._config, current_user=upload_permission.author, session=self._session) self._notify_uploaded_contents( uploader_username=uploader_username, workspace_in_context=workspace_lib.get_workspace_with_context( upload_permission.workspace), uploader_message=message, uploaded_contents=created_contents, uploader_email=upload_permission.email, ) return created_contents
def notify_content_update( self, event_actor_id: int, event_content_id: int ) -> None: """ Look for all users to be notified about the new content and send them an individual email :param event_actor_id: id of the user that has triggered the event :param event_content_id: related content_id :return: """ # FIXME - D.A. - 2014-11-05 # Dirty import. It's here in order to avoid circular import from tracim_backend.lib.core.content import ContentApi from tracim_backend.lib.core.user import UserApi user = UserApi( None, config=self.config, session=self.session, ).get_one(event_actor_id) logger.debug(self, 'Content: {}'.format(event_content_id)) content_api = ContentApi( current_user=user, session=self.session, config=self.config, ) content = ContentApi( session=self.session, current_user=user, # nopep8 TODO - use a system user instead of the user that has triggered the event config=self.config, show_archived=True, show_deleted=True, ).get_one(event_content_id, content_type_list.Any_SLUG) workspace_api = WorkspaceApi( session=self.session, current_user=user, config=self.config, ) workpace_in_context = workspace_api.get_workspace_with_context(workspace_api.get_one(content.workspace_id)) # nopep8 main_content = content.parent if content.type == content_type_list.Comment.slug else content # nopep8 notifiable_roles = WorkspaceApi( current_user=user, session=self.session, config=self.config, ).get_notifiable_roles(content.workspace) if len(notifiable_roles) <= 0: logger.info(self, 'Skipping notification as nobody subscribed to in workspace {}'.format(content.workspace.label)) return logger.info(self, 'Generating content {} notification email for {} user(s)'.format( content.content_id, len(notifiable_roles) )) # INFO - D.A. - 2014-11-06 # The following email sender will send emails in the async task queue # This allow to build all mails through current thread but really send them (including SMTP connection) # In the other thread. # # This way, the webserver will return sooner (actually before notification emails are sent email_sender = EmailSender( self.config, self._smtp_config, self.config.EMAIL_NOTIFICATION_ACTIVATED ) for role in notifiable_roles: logger.info(self, 'Generating content {} notification email to {}'.format( content.content_id, role.user.email ) ) translator = Translator(app_config=self.config, default_lang=role.user.lang) # nopep8 _ = translator.get_translation to_addr = formataddr((role.user.display_name, role.user.email)) # INFO - G.M - 2017-11-15 - set content_id in header to permit reply # references can have multiple values, but only one in this case. replyto_addr = self.config.EMAIL_NOTIFICATION_REPLY_TO_EMAIL.replace( # nopep8 '{content_id}', str(main_content.content_id) ) reference_addr = self.config.EMAIL_NOTIFICATION_REFERENCES_EMAIL.replace( #nopep8 '{content_id}',str(main_content.content_id) ) # # INFO - D.A. - 2014-11-06 # We do not use .format() here because the subject defined in the .ini file # may not include all required labels. In order to avoid partial format() (which result in an exception) # we do use replace and force the use of .__str__() in order to process LazyString objects # content_status = translator.get_translation(main_content.get_status().label) translated_subject = translator.get_translation(self.config.EMAIL_NOTIFICATION_CONTENT_UPDATE_SUBJECT) subject = translated_subject.replace(EST.WEBSITE_TITLE, self.config.WEBSITE_TITLE.__str__()) subject = subject.replace(EST.WORKSPACE_LABEL, main_content.workspace.label.__str__()) subject = subject.replace(EST.CONTENT_LABEL, main_content.label.__str__()) subject = subject.replace(EST.CONTENT_STATUS_LABEL, content_status) reply_to_label = _('{username} & all members of {workspace}').format( # nopep8 username=user.display_name, workspace=main_content.workspace.label) message = MIMEMultipart('alternative') message['Subject'] = subject message['From'] = self._get_sender(user) message['To'] = to_addr message['Reply-to'] = formataddr((reply_to_label, replyto_addr)) # INFO - G.M - 2017-11-15 # References can theorically have label, but in pratice, references # contains only message_id from parents post in thread. # To link this email to a content we create a virtual parent # in reference who contain the content_id. message['References'] = formataddr(('', reference_addr)) content_in_context = content_api.get_content_in_context(content) parent_in_context = None if content.parent_id: parent_in_context = content_api.get_content_in_context(content.parent) # nopep8 body_html = self._build_email_body_for_content( self.config.EMAIL_NOTIFICATION_CONTENT_UPDATE_TEMPLATE_HTML, role, content_in_context, parent_in_context, workpace_in_context, user, translator, ) part2 = MIMEText(body_html, 'html', 'utf-8') # Attach parts into message container. # According to RFC 2046, the last part of a multipart message, in this case # the HTML message, is best and preferred. message.attach(part2) self.log_email_notification( msg='an email was created to {}'.format(message['To']), action='{:8s}'.format('CREATED'), email_recipient=message['To'], email_subject=message['Subject'], config=self.config, ) send_email_through( self.config, email_sender.send_mail, message )
def notify_content_update(self, event_actor_id: int, event_content_id: int) -> None: """ Look for all users to be notified about the new content and send them an individual email :param event_actor_id: id of the user that has triggered the event :param event_content_id: related content_id :return: """ # FIXME - D.A. - 2014-11-05 # Dirty import. It's here in order to avoid circular import from tracim_backend.lib.core.content import ContentApi from tracim_backend.lib.core.user import UserApi user = UserApi(None, config=self.config, session=self.session).get_one(event_actor_id) logger.debug(self, "Content: {}".format(event_content_id)) content_api = ContentApi(current_user=user, session=self.session, config=self.config) content = ContentApi( session=self.session, current_user=user, # TODO - G.M - 2019-04-24 - use a system user instead of the user that has triggered the event config=self.config, show_archived=True, show_deleted=True, ).get_one(event_content_id, content_type_list.Any_SLUG) workspace_api = WorkspaceApi(session=self.session, current_user=user, config=self.config) workpace_in_context = workspace_api.get_workspace_with_context( workspace_api.get_one(content.workspace_id)) main_content = content.parent if content.type == content_type_list.Comment.slug else content notifiable_roles = WorkspaceApi( current_user=user, session=self.session, config=self.config).get_notifiable_roles(content.workspace) if len(notifiable_roles) <= 0: logger.info( self, "Skipping notification as nobody subscribed to in workspace {}" .format(content.workspace.label), ) return logger.info( self, "Generating content {} notification email for {} user(s)".format( content.content_id, len(notifiable_roles)), ) # INFO - D.A. - 2014-11-06 # The following email sender will send emails in the async task queue # This allow to build all mails through current thread but really send them (including SMTP connection) # In the other thread. # # This way, the webserver will return sooner (actually before notification emails are sent email_sender = EmailSender(self.config, self._smtp_config, self.config.EMAIL__NOTIFICATION__ACTIVATED) for role in notifiable_roles: logger.info( self, "Generating content {} notification email to {}".format( content.content_id, role.user.email), ) translator = Translator(app_config=self.config, default_lang=role.user.lang) _ = translator.get_translation # INFO - G.M - 2017-11-15 - set content_id in header to permit reply # references can have multiple values, but only one in this case. replyto_addr = self.config.EMAIL__NOTIFICATION__REPLY_TO__EMAIL.replace( "{content_id}", str(main_content.content_id)) reference_addr = self.config.EMAIL__NOTIFICATION__REFERENCES__EMAIL.replace( "{content_id}", str(main_content.content_id)) # # INFO - D.A. - 2014-11-06 # We do not use .format() here because the subject defined in the .ini file # may not include all required labels. In order to avoid partial format() (which result in an exception) # we do use replace and force the use of .__str__() in order to process LazyString objects # content_status = translator.get_translation( main_content.get_status().label) translated_subject = translator.get_translation( self.config.EMAIL__NOTIFICATION__CONTENT_UPDATE__SUBJECT) subject = translated_subject.replace( EST.WEBSITE_TITLE, self.config.WEBSITE__TITLE.__str__()) subject = subject.replace(EST.WORKSPACE_LABEL, main_content.workspace.label.__str__()) subject = subject.replace(EST.CONTENT_LABEL, main_content.label.__str__()) subject = subject.replace(EST.CONTENT_STATUS_LABEL, content_status) reply_to_label = _( "{username} & all members of {workspace}").format( username=user.display_name, workspace=main_content.workspace.label) content_in_context = content_api.get_content_in_context(content) parent_in_context = None if content.parent_id: parent_in_context = content_api.get_content_in_context( content.parent) body_html = self._build_email_body_for_content( self.config. EMAIL__NOTIFICATION__CONTENT_UPDATE__TEMPLATE__HTML, role, content_in_context, parent_in_context, workpace_in_context, user, translator, ) message = EmailNotificationMessage( subject=subject, from_header=self._get_sender(user), to_header=EmailAddress(role.user.display_name, role.user.email), reply_to=EmailAddress(reply_to_label, replyto_addr), # INFO - G.M - 2017-11-15 # References can theorically have label, but in pratice, references # contains only message_id from parents post in thread. # To link this email to a content we create a virtual parent # in reference who contain the content_id. # INFO - G.M - 2020-04-03 - Enforce angle bracket in references header # we need that to ensure best software compatibility # compat from parsing software references=EmailAddress("", reference_addr, force_angle_bracket=True), body_html=body_html, lang=translator.default_lang, ) self.log_email_notification( msg="an email was created to {}".format(message["To"]), action="{:8s}".format("CREATED"), email_recipient=message["To"], email_subject=message["Subject"], config=self.config, ) send_email_through(self.config, email_sender.send_mail, message)