def find_key_from_mail_address( cls, mail_address: str, pattern: str, marker_str: str ) -> typing.Optional[str]: """ Parse mail_adress-like string to retrieve key. :param mail_address: mail_adress like user+key@something / key@something :param pattern: pattern like user+{marker_str}@something :param marker_str: marker_name with bracket like {content_id} :return: key """ # splitting pattern with marker_str, # ex with {content_id} as marker_str # noreply+{content_id}@website.tld -> ['noreply+','@website.tld'] static_parts = pattern.split(marker_str) assert len(static_parts) > 1 assert len(static_parts) < 3 if len(static_parts) == 2: before, after = static_parts if mail_address.startswith(before) and mail_address.endswith(after): key = mail_address.replace(before, "").replace(after, "") assert key.isalnum() return key logger.warning( cls, "pattern {} does not match email address {} ".format(pattern, mail_address) ) return None
def execute_created_user_actions(self, user: User) -> None: """ WARNING! This method will be deprecated soon, see https://github.com/tracim/tracim/issues/1589 and https://github.com/tracim/tracim/issues/1487 This method do post-create user actions """ # FIXME - G.M - 2019-03-18 - move this code to another place when # event mechanism is ready, see https://github.com/tracim/tracim/issues/1487 # event on_created_user should start hook use by agenda app code. app_lib = ApplicationApi(app_list=app_list) if app_lib.exist(AGENDA__APP_SLUG): agenda_api = AgendaApi(current_user=self._user, session=self._session, config=self._config) try: agenda_already_exist = agenda_api.ensure_user_agenda_exists( user) if agenda_already_exist: logger.warning( self, "user {} has just been created but their own agenda already exists" .format(user.user_id), ) except AgendaServerConnectionError as exc: logger.error(self, "Cannot connect to the agenda server") logger.exception(self, exc) except Exception as exc: logger.error( self, "Something went wrong during agenda create/update") logger.exception(self, exc)
def add_permission_to_workspace( self, workspace: Workspace, emails: typing.List[str], password: typing.Optional[str] = None, do_notify=False, ) -> typing.List[UploadPermission]: upload_permissions = [] created = datetime.utcnow() upload_permission_group_uuid = str(uuid.uuid4().hex) for email in emails: upload_permission = UploadPermission( author=self._user, workspace_id=workspace.workspace_id, email=email.lower(), token=str(uuid.uuid4()), password=password, type=UploadPermissionType.EMAIL, created=created, upload_permission_group_uuid=upload_permission_group_uuid, enabled=True, ) self.save(upload_permission) upload_permissions.append(upload_permission) self._session.flush() if do_notify: userlib = UserApi(config=self._config, current_user=self._user, session=self._session) workspace_lib = WorkspaceApi(config=self._config, current_user=self._user, session=self._session) try: email_manager = self._get_email_manager( self._config, self._session) email_manager.notify_upload_permission( emitter=userlib.get_user_with_context(self._user), workspace_in_context=workspace_lib. get_workspace_with_context(workspace), upload_permission_receivers=self. get_upload_permissions_in_context(upload_permissions), upload_permission_password=password, ) # FIXME - G.M - 2018-11-02 - hack: accept bad recipient user creation # this should be fixed to find a solution to allow "fake" email but # also have clear error case for valid mail. except SMTPRecipientsRefused: logger.warning( self, "Upload Permission initied by user {} but SMTP " "server refuse to send notification".format( self._user.login), ) except SMTPException as exc: raise NotificationSendingFailed( "Notification for Upload Permission can't be send " "(SMTP error).") from exc return upload_permissions
def share_content( self, content: Content, emails: typing.List[str], password: typing.Optional[str] = None, do_notify: bool = False, ) -> typing.List[ContentShare]: content_shares = [] created = datetime.now() share_group_uuid = str(uuid.uuid4()) for email in emails: content_share = ContentShare( author=self._user, content_id=content.content_id, email=email.lower(), share_token=str(uuid.uuid4()), password=password, type=ContentShareType.EMAIL, created=created, share_group_uuid=share_group_uuid, enabled=True, ) self.save(content_share) content_shares.append(content_share) self._session.flush() if do_notify: api = ContentApi(config=self._config, session=self._session, current_user=self._user) content_in_context = api.get_content_in_context(content) try: email_manager = self._get_email_manager( self._config, self._session) email_manager.notify__share__content( emitter=self._user, shared_content=content_in_context, content_share_receivers=self.get_content_shares_in_context( content_shares), share_password=password, ) # FIXME - G.M - 2018-11-02 - hack: accept bad recipient user creation # this should be fixed to find a solution to allow "fake" email but # also have clear error case for valid mail. except SMTPRecipientsRefused: logger.warning( self, "Share initied by {email} but SMTP server refuse to send notification" .format(email=self._user.email), ) except SMTPException as exc: raise NotificationSendingFailed( "Notification for shared file can't be send " "(SMTP error).") from exc return content_shares
def create_user( self, email, password: str = None, name: str = None, timezone: str = '', lang: str= None, auth_type: AuthType = AuthType.UNKNOWN, groups=[], do_save: bool=True, do_notify: bool=True, ) -> User: if do_notify and not self._config.EMAIL_NOTIFICATION_ACTIVATED: raise NotificationDisabledCantCreateUserWithInvitation( "Can't create user with invitation mail because " "notification are disabled." ) new_user = self.create_minimal_user(email, groups, save_now=False) self.update( user=new_user, name=name, email=email, auth_type=auth_type, password=password, timezone=timezone, lang=lang, do_save=False, ) if do_notify: try: email_manager = get_email_manager(self._config, self._session) email_manager.notify_created_account( new_user, password=password ) # FIXME - G.M - 2018-11-02 - hack: accept bad recipient user creation # this should be fixed to find a solution to allow "fake" email but # also have clear error case for valid mail. except SMTPRecipientsRefused as exc: logger.warning( self, "Account created for {email} but SMTP server refuse to send notification".format( # nopep8 email=email ) ) except SMTPException as exc: raise NotificationSendingFailed( "Notification for new created account can't be send " "(SMTP error), new account creation aborted" ) from exc if do_save: self.save(new_user) return new_user
def create_user( self, email, password: str = None, name: str = None, timezone: str = "", lang: str = None, auth_type: AuthType = AuthType.UNKNOWN, profile: typing.Optional[Profile] = None, allowed_space: typing.Optional[int] = None, do_save: bool = True, do_notify: bool = True, ) -> User: if do_notify and not self._config.EMAIL__NOTIFICATION__ACTIVATED: raise NotificationDisabledCantCreateUserWithInvitation( "Can't create user with invitation mail because " "notification are disabled.") new_user = self.create_minimal_user(email, profile, save_now=False) if allowed_space is None: allowed_space = self._config.LIMITATION__USER_DEFAULT_ALLOWED_SPACE self.update( user=new_user, name=name, email=email, auth_type=auth_type, password=password, timezone=timezone, allowed_space=allowed_space, lang=lang, do_save=False, ) if do_notify: try: email_manager = get_email_manager(self._config, self._session) email_manager.notify_created_account(new_user, password=password, origin_user=self._user) # FIXME - G.M - 2018-11-02 - hack: accept bad recipient user creation # this should be fixed to find a solution to allow "fake" email but # also have clear error case for valid mail. except SMTPRecipientsRefused: logger.warning( self, "Account created for {email} but SMTP server refuse to send notification" .format(email=email), ) except SMTPException as exc: raise NotificationSendingFailed( "Notification for new created account can't be send " "(SMTP error), new account creation aborted") from exc if do_save: self.save(new_user) return new_user
def determine_requested_mode(self, request: "TracimRequest") -> DavAuthorization: if request.method in CALDAV_READ_METHODS: return DavAuthorization.READ elif request.method in CALDAV_WRITE_METHODS: return DavAuthorization.WRITE elif request.method in CALDAV_MANAGER_METHODS: return DavAuthorization.MANAGER else: logger.warning( self, 'Unknown http method "{}" authorization will be MANAGER'.format(request.method), ) return DavAuthorization.MANAGER
def get_raw_config( self, config_file_name: str, default_value: typing.Optional[str] = None, secret: bool = False, deprecated: bool = False, deprecated_extended_information: str = "", ) -> str: """ Get config parameter according to a config name. Priority: - 1: Environement variable - 2: Config file data (stored in CFG.settings dict) - 3: default_value :param config_file_name: name of the config parameter name :param default_value: default value if not setted value found :param secret: is the value of the parameter secret ? (if true, it will not be printed) :param deprecated: is the parameter deprecated ? :param deprecated_extended_information: some more information about deprecated parameter :return: """ param = ConfigParam( config_file_name=config_file_name, secret=secret, default_value=default_value, settings=self.settings, deprecated=deprecated, deprecated_extended_information=deprecated_extended_information, ) self.config_info.append(param) logger.info( self, CONFIG_LOG_TEMPLATE.format( config_value=param.config_value, config_source=param.config_source, config_name=param.config_name, config_name_source=param.config_name_source, ), ) if param.deprecated and param.config_value: logger.warning( self, "{parameter_name} parameter is deprecated. {extended_information}" .format( parameter_name=param.config_name, extended_information=param.deprecated_extended_information, ), ) return param.real_config_value
def _notify_tracim(self, mails: typing.List[DecodedMail], imapc: imapclient.IMAPClient) -> None: """ Send http request to tracim endpoint :param mails: list of mails to send :return: none """ logger.debug(self, "Notify tracim about {} new responses".format(len(mails))) # TODO BS 20171124: Look around mail.get_from_address(), mail.get_key() # , mail.get_body() etc ... for raise InvalidEmailError if missing # required informations (actually get_from_address raise IndexError # if no from address for example) and catch it here while mails: mail = mails.pop() try: method, endpoint, json_body_dict = self._create_comment_request( mail) except NoKeyFound: log = "Failed to create comment request due to missing specialkey in mail" logger.exception(self, log) continue except EmptyEmailBody: log = "Empty body, skip mail" logger.error(self, log) continue except AutoReplyEmailNotAllowed: log = "Autoreply mail, skip mail" logger.warning(self, log) continue except Exception: log = "Failed to create comment request in mail fetcher error" logger.exception(self, log) continue try: self._send_request( mail=mail, imapc=imapc, method=method, endpoint=endpoint, json_body_dict=json_body_dict, ) except requests.exceptions.Timeout: log = "Timeout error to transmit fetched mail to tracim" logger.exception(self, log) except requests.exceptions.RequestException: log = "Fail to transmit fetched mail to tracim" logger.exception(self, log)
def _register_all(plugin_manager: pluggy.PluginManager, plugins: typing.Dict[str, types.ModuleType]): # INFO - G.M - 2019-12-02 - sort plugins by name here to have predictable # order for plugin registration according to plugin_name. plugins = collections.OrderedDict(sorted(plugins.items())) for plugin_name, module in plugins.items(): plugin_manager.register(module) try: entry_point = getattr(module, PLUGIN_ENTRY_POINT_NAME) entry_point(plugin_manager) except AttributeError: logger.warning( plugin_manager, "Cannot find plugin entry point '{}' in '{}' plugin".format( PLUGIN_ENTRY_POINT_NAME, plugin_name), )
def size(self) -> typing.Optional[int]: """ :return: size of content if available, None if unavailable """ if not self.revision.depot_file: return None try: return self.revision.depot_file.file.content_length except IOError: logger.warning( self, "IO Exception Occured when trying to get content size", exc_info=True ) except Exception: logger.warning( self, "Unknown Exception Occured when trying to get content size", exc_info=True ) return None
def create_user( self, email, password: str = None, name: str = None, timezone: str = '', lang: str = None, groups=[], do_save: bool = True, do_notify: bool = True, ) -> User: if do_notify and not self._config.EMAIL_NOTIFICATION_ACTIVATED: raise NotificationDisabledCantCreateUserWithInvitation( "Can't create user with invitation mail because " "notification are disabled.") new_user = self.create_minimal_user(email, groups, save_now=False) self.update( user=new_user, name=name, email=email, password=password, timezone=timezone, lang=lang, do_save=False, ) if do_notify: try: email_manager = get_email_manager(self._config, self._session) email_manager.notify_created_account(new_user, password=password) # FIXME - G.M - 2018-11-02 - hack: accept bad recipient user creation # this should be fixed to find a solution to allow "fake" email but # also have clear error case for valid mail. except SMTPRecipientsRefused as exc: logger.warning( self, "Account created for {email} but SMTP server refuse to send notification" .format( # nopep8 email=email)) except SMTPException as exc: raise NotificationSendingFailed( "Notification for new created account can't be send " "(SMTP error), new account creation aborted") from exc if do_save: self.save(new_user) return new_user
def _check_email_config_validity(self) -> None: """ Check if config is correctly setted for email features """ if not self.EMAIL__NOTIFICATION__ACTIVATED: logger.warning( self, "Notification by email mecanism is disabled ! " "Notification and mail invitation mecanisms will not work.", ) if not self.EMAIL__REPLY__LOCKFILE_PATH and self.EMAIL__REPLY__ACTIVATED: self.check_mandatory_param( "EMAIL__REPLY__LOCKFILE_PATH", self.EMAIL__REPLY__LOCKFILE_PATH, when_str="when email reply is activated", ) # INFO - G.M - 2019-02-01 - check if template are available, # do not allow running with email_notification_activated # if templates needed are not available if self.EMAIL__NOTIFICATION__ACTIVATED: templates = { "content_update notification": self.EMAIL__NOTIFICATION__CONTENT_UPDATE__TEMPLATE__HTML, "created account": self.EMAIL__NOTIFICATION__CREATED_ACCOUNT__TEMPLATE__HTML, "password reset": self. EMAIL__NOTIFICATION__RESET_PASSWORD_REQUEST__TEMPLATE__HTML, } for template_description, template_path in templates.items(): if not template_path or not os.path.isfile(template_path): raise ConfigurationError( "ERROR: email template for {template_description} " 'not found at "{template_path}."'.format( template_description=template_description, template_path=template_path)) if self.EMAIL__PROCESSING_MODE not in (self.CST.ASYNC, self.CST.SYNC): raise Exception("EMAIL__PROCESSING_MODE " "can " 'be "{}" or "{}", not "{}"'.format( self.CST.ASYNC, self.CST.SYNC, self.EMAIL__PROCESSING_MODE))
def execute_created_workspace_actions(self, workspace: Workspace) -> None: """ WARNING ! This method Will be Deprecated soon, see https://github.com/tracim/tracim/issues/1589 and https://github.com/tracim/tracim/issues/1487 This method do post creation workspace actions """ # FIXME - G.M - 2019-03-18 - move this code to another place when # event mechanism is ready, see https://github.com/tracim/tracim/issues/1487 # event on_created_workspace should start hook use by agenda app code. # TODO - G.M - 2019-04-11 - Circular Import, will probably be remove # with event refactor, see https://github.com/tracim/tracim/issues/1487 from tracim_backend.applications.agenda.lib import AgendaApi app_lib = ApplicationApi(app_list=app_list) if app_lib.exist(AGENDA__APP_SLUG): if workspace.agenda_enabled: agenda_api = AgendaApi(current_user=self._user, session=self._session, config=self._config) try: agenda_already_exist = agenda_api.ensure_workspace_agenda_exists( workspace) if agenda_already_exist: logger.warning( self, "workspace {} is just created but it own agenda already exist !!" .format(workspace.workspace_id), ) except AgendaServerConnectionError as exc: logger.error(self, "Cannot connect to agenda server") logger.exception(self, exc) except Exception as exc: logger.error( self, "Something goes wrong during agenda create/update") logger.exception(self, exc)
def execute_created_user_actions(self, user: User) -> None: """ WARNING ! This method Will be Deprecated soon, see https://github.com/tracim/tracim/issues/1589 and https://github.com/tracim/tracim/issues/1487 This method do post-create user actions """ # TODO - G.M - 04-04-2018 - [auth] # Check if this is already needed with # new auth system user.ensure_auth_token( validity_seconds=self._config.USER__AUTH_TOKEN__VALIDITY) # FIXME - G.M - 2019-03-18 - move this code to another place when # event mechanism is ready, see https://github.com/tracim/tracim/issues/1487 # event on_created_user should start hook use by agenda app code. app_lib = ApplicationApi(app_list=app_list) if app_lib.exist(AGENDA__APP_SLUG): agenda_api = AgendaApi(current_user=self._user, session=self._session, config=self._config) try: agenda_already_exist = agenda_api.ensure_user_agenda_exists( user) if agenda_already_exist: logger.warning( self, "user {} is just created but his own agenda already exist !!" .format(user.user_id), ) except AgendaServerConnectionError as exc: logger.error(self, "Cannot connect to agenda server") logger.exception(self, exc) except Exception as exc: logger.error( self, "Something goes wrong during agenda create/update") logger.exception(self, exc)
def size(self) -> typing.Optional[int]: """ :return: size of content if available, None if unavailable """ if not self.content.depot_file: return None try: return self.content.depot_file.file.content_length except IOError as e: logger.warning( self, "IO Exception Occured when trying to get content size : {}".format(str(e)) ) logger.warning(self, traceback.format_exc()) except Exception as e: logger.warning( self, "Unknown Exception Occured when trying to get content size : {}".format(str(e)) ) logger.warning(self, traceback.format_exc()) return None
def run(self) -> None: logger.info(self, "Starting MailFetcher") while self._is_active: imapc = None sleep_after_connection = True try: imapc = imapclient.IMAPClient( self.host, self.port, ssl=self.use_ssl, timeout=MAIL_FETCHER_CONNECTION_TIMEOUT ) imapc.login(self.user, self.password) logger.debug(self, "Select folder {}".format(self.folder)) imapc.select_folder(self.folder) # force renew connection when deadline is reached deadline = time.time() + self.connection_max_lifetime while True: if not self._is_active: logger.warning(self, "Mail Fetcher process aborted") sleep_after_connection = False break if time.time() > deadline: logger.debug( self, "MailFetcher Connection Lifetime limit excess" ", Try Re-new connection", ) sleep_after_connection = False break # check for new mails self._check_mail(imapc) if self.use_idle and imapc.has_capability("IDLE"): # IDLE_mode wait until event from server logger.debug(self, "wail for event(IDLE)") imapc.idle() imapc.idle_check(timeout=MAIL_FETCHER_IDLE_RESPONSE_TIMEOUT) imapc.idle_done() else: if self.use_idle and not imapc.has_capability("IDLE"): log = ( "IDLE mode activated but server do not" "support it, use polling instead." ) logger.warning(self, log) if self.burst: self.stop() break # normal polling mode : sleep a define duration logger.debug(self, "sleep for {}".format(self.heartbeat)) time.sleep(self.heartbeat) if self.burst: self.stop() break # Socket except (socket.error, socket.gaierror, socket.herror) as e: log = "Socket fail with IMAP connection {}" logger.error(self, log.format(e.__str__())) except socket.timeout as e: log = "Socket timeout on IMAP connection {}" logger.error(self, log.format(e.__str__())) # SSL except ssl.SSLError as e: log = "SSL error on IMAP connection" logger.error(self, log.format(e.__str__())) except ssl.CertificateError as e: log = "SSL Certificate verification failed on IMAP connection" logger.error(self, log.format(e.__str__())) # Filelock except filelock.Timeout as e: log = "Mail Fetcher Lock Timeout {}" logger.warning(self, log.format(e.__str__())) # IMAP # TODO - G.M - 10-01-2017 - Support imapclient exceptions # when Imapclient stable will be 2.0+ except BadIMAPFetchResponse as e: log = ( "Imap Fetch command return bad response." "Is someone else connected to the mailbox ?: " "{}" ) logger.error(self, log.format(e.__str__())) # Others except Exception as e: log = "Mail Fetcher error {}" logger.error(self, log.format(e.__str__())) finally: # INFO - G.M - 2018-01-09 - Connection closing # Properly close connection according to # https://github.com/mjs/imapclient/pull/279/commits/043e4bd0c5c775c5a08cb5f1baa93876a46732ee # TODO : Use __exit__ method instead when imapclient stable will # be 2.0+ . if imapc: logger.debug(self, "Try logout") try: imapc.logout() except Exception: try: imapc.shutdown() except Exception as e: log = "Can't logout, connection broken ? {}" logger.error(self, log.format(e.__str__())) if self.burst: self.stop() break if sleep_after_connection: logger.debug(self, "sleep for {}".format(self.heartbeat)) time.sleep(self.heartbeat) log = "Mail Fetcher stopped" logger.debug(self, log)
def append_thread_callback(self, callback: typing.Callable) -> None: logger.warning('MailSenderDaemon not implement append_thread_callback') pass
def __init__(self, settings: typing.Dict[str, typing.Any]): """Parse configuration file.""" ### # General ### backend_folder = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) # nopep8 tracim_v2_folder = os.path.dirname(backend_folder) default_color_config_file_path = os.path.join(tracim_v2_folder, 'color.json') # nopep8 self.COLOR_CONFIG_FILE_PATH = settings.get( 'color.config_file_path', default_color_config_file_path ) if not os.path.exists(self.COLOR_CONFIG_FILE_PATH): raise Exception( 'ERROR: {} file does not exist. ' 'please create it or set color.config_file_path' 'with a correct value'.format(self.COLOR_CONFIG_FILE_PATH) ) try: with open(self.COLOR_CONFIG_FILE_PATH) as json_file: self.APPS_COLORS = json.load(json_file) except Exception as e: raise Exception( 'Error: {} file could not be load as json'.format(self.COLOR_CONFIG_FILE_PATH) # nopep8 ) from e try: self.APPS_COLORS['primary'] except KeyError as e: raise Exception( 'Error: primary color is required in {} file'.format( self.COLOR_CONFIG_FILE_PATH) # nopep8 ) from e default_enabled_app = [ 'contents/thread', 'contents/file', 'contents/html-document', 'contents/folder', ] enabled_app = [] enabled_app_str = settings.get('app.enabled', None) if enabled_app_str: for app in enabled_app_str.split(','): app_name = app.strip() enabled_app.append(app_name) else: enabled_app = default_enabled_app self.ENABLED_APP = enabled_app self._set_default_app(self.ENABLED_APP) mandatory_msg = \ 'ERROR: {} configuration is mandatory. Set it before continuing.' self.DEPOT_STORAGE_DIR = settings.get( 'depot_storage_dir', ) if not self.DEPOT_STORAGE_DIR: raise Exception( mandatory_msg.format('depot_storage_dir') ) self.DEPOT_STORAGE_NAME = settings.get( 'depot_storage_name', ) if not self.DEPOT_STORAGE_NAME: raise Exception( mandatory_msg.format('depot_storage_name') ) self.PREVIEW_CACHE_DIR = settings.get( 'preview_cache_dir', ) if not self.PREVIEW_CACHE_DIR: raise Exception( 'ERROR: preview_cache_dir configuration is mandatory. ' 'Set it before continuing.' ) auth_type_str = settings.get( 'auth_types', 'internal' ) self.AUTH_TYPES = [AuthType(auth.strip()) for auth in auth_type_str.split(',')] if AuthType.REMOTE is self.AUTH_TYPES: raise Exception( 'ERROR: "remote" auth not allowed in auth_types' ' list, use remote_user_header instead' ) self.REMOTE_USER_HEADER = settings.get('remote_user_header', None) # TODO - G.M - 2018-09-11 - Deprecated param # self.DATA_UPDATE_ALLOWED_DURATION = int(settings.get( # 'content.update.allowed.duration', # 0, # )) self.API_KEY = settings.get( 'api.key', '' ) self.SESSION_REISSUE_TIME = int(settings.get( 'session.reissue_time', 120 )) self.WEBSITE_TITLE = settings.get( 'website.title', 'TRACIM', ) # base url of the frontend self.WEBSITE_BASE_URL = settings.get( 'website.base_url', '', ) if not self.WEBSITE_BASE_URL: raise Exception( 'website.base_url is needed in order to have correct path in' 'few place like in email.' 'You should set it with frontend root url.' ) self.API_BASE_URL = settings.get( 'api.base_url', self.WEBSITE_BASE_URL, ) allowed_origin = [] allowed_origin_string = settings.get( 'cors.access-control-allowed-origin', '' ) if allowed_origin_string: allowed_origin.extend(allowed_origin_string.split(',')) # nopep8 if not allowed_origin: allowed_origin.append(self.WEBSITE_BASE_URL) if self.API_BASE_URL != self.WEBSITE_BASE_URL: allowed_origin.append(self.API_BASE_URL) self.CORS_ALLOWED_ORIGIN = allowed_origin # TODO - G.M - 26-03-2018 - [Cleanup] These params seems deprecated for tracimv2, # nopep8 # Verify this # # self.WEBSITE_HOME_TITLE_COLOR = settings.get( # 'website.title.color', # '#555', # ) # self.WEBSITE_HOME_IMAGE_PATH = settings.get( # '/assets/img/home_illustration.jpg', # ) # self.WEBSITE_HOME_BACKGROUND_IMAGE_PATH = settings.get( # '/assets/img/bg.jpg', # ) # website_server_name = settings.get( 'website.server_name', None, ) if not website_server_name: website_server_name = urlparse(self.WEBSITE_BASE_URL).hostname logger.warning( self, 'NOTE: Generated website.server_name parameter from ' 'website.base_url parameter -> {0}' .format(website_server_name) ) self.WEBSITE_SERVER_NAME = website_server_name # TODO - G.M - 2018-09-11 - Deprecated params # self.WEBSITE_HOME_TAG_LINE = settings.get( # 'website.home.tag_line', # '', # ) # self.WEBSITE_SUBTITLE = settings.get( # 'website.home.subtitle', # '', # ) # self.WEBSITE_HOME_BELOW_LOGIN_FORM = settings.get( # 'website.home.below_login_form', # '', # ) # # self.WEBSITE_TREEVIEW_CONTENT = settings.get( # 'website.treeview.content', # ) self.USER_AUTH_TOKEN_VALIDITY = int(settings.get( 'user.auth_token.validity', '604800', )) self.USER_RESET_PASSWORD_TOKEN_VALIDITY = int(settings.get( 'user.reset_password.validity', '900' )) self.DEBUG = asbool(settings.get('debug', False)) # TODO - G.M - 27-03-2018 - [Email] Restore email config ### # EMAIL related stuff (notification, reply) ## self.EMAIl_NOTIFICATION_ENABLED_ON_INVITATION = asbool(settings.get( 'email.notification.enabled_on_invitation', True )) self.EMAIL_NOTIFICATION_NOTIFIED_EVENTS = [ ActionDescription.COMMENT, ActionDescription.CREATION, ActionDescription.EDITION, ActionDescription.REVISION, ActionDescription.STATUS_UPDATE ] self.EMAIL_NOTIFICATION_NOTIFIED_CONTENTS = [ content_type_list.Page.slug, content_type_list.Thread.slug, content_type_list.File.slug, content_type_list.Comment.slug, # content_type_list.Folder.slug -- Folder is skipped ] if settings.get('email.notification.from'): raise Exception( 'email.notification.from configuration is deprecated. ' 'Use instead email.notification.from.email and ' 'email.notification.from.default_label.' ) self.EMAIL_NOTIFICATION_FROM_EMAIL = settings.get( 'email.notification.from.email', 'noreply+{user_id}@trac.im' ) self.EMAIL_NOTIFICATION_FROM_DEFAULT_LABEL = settings.get( 'email.notification.from.default_label', 'Tracim Notifications' ) self.EMAIL_NOTIFICATION_REPLY_TO_EMAIL = settings.get( 'email.notification.reply_to.email', ) self.EMAIL_NOTIFICATION_REFERENCES_EMAIL = settings.get( 'email.notification.references.email' ) # Content update notification self.EMAIL_NOTIFICATION_CONTENT_UPDATE_TEMPLATE_HTML = settings.get( 'email.notification.content_update.template.html', ) self.EMAIL_NOTIFICATION_CONTENT_UPDATE_SUBJECT = settings.get( 'email.notification.content_update.subject', _("[{website_title}] [{workspace_label}] {content_label} ({content_status_label})") # nopep8 ) # Created account notification self.EMAIL_NOTIFICATION_CREATED_ACCOUNT_TEMPLATE_HTML = settings.get( 'email.notification.created_account.template.html', ) self.EMAIL_NOTIFICATION_CREATED_ACCOUNT_SUBJECT = settings.get( 'email.notification.created_account.subject', _('[{website_title}] Someone created an account for you'), ) # Reset password notification self.EMAIL_NOTIFICATION_RESET_PASSWORD_TEMPLATE_HTML = settings.get( 'email.notification.reset_password_request.template.html', ) self.EMAIL_NOTIFICATION_RESET_PASSWORD_SUBJECT = settings.get( 'email.notification.reset_password_request.subject', _('[{website_title}] A password reset has been requested'), ) self.EMAIL_NOTIFICATION_PROCESSING_MODE = settings.get( 'email.notification.processing_mode', ) self.EMAIL_NOTIFICATION_ACTIVATED = asbool(settings.get( 'email.notification.activated', )) if not self.EMAIL_NOTIFICATION_ACTIVATED: logger.warning( self, 'Notification by email mecanism is disabled ! ' 'Notification and mail invitation mecanisms will not work.' ) # INFO - G.M - 2019-02-01 - check if template are available, # do not allow running with email_notification_activated # if templates needed are not available if self.EMAIL_NOTIFICATION_ACTIVATED: templates = { 'content_update notification': self.EMAIL_NOTIFICATION_CONTENT_UPDATE_TEMPLATE_HTML, 'created account': self.EMAIL_NOTIFICATION_CREATED_ACCOUNT_TEMPLATE_HTML, 'password reset': self.EMAIL_NOTIFICATION_RESET_PASSWORD_TEMPLATE_HTML } for template_description, template_path in templates.items(): if not template_path or not os.path.isfile(template_path): raise ConfigurationError( 'ERROR: email template for {template_description} ' 'not found at "{template_path}."'.format( template_description=template_description, template_path=template_path ) ) self.EMAIL_NOTIFICATION_SMTP_SERVER = settings.get( 'email.notification.smtp.server', ) self.EMAIL_NOTIFICATION_SMTP_PORT = settings.get( 'email.notification.smtp.port', ) self.EMAIL_NOTIFICATION_SMTP_USER = settings.get( 'email.notification.smtp.user', ) self.EMAIL_NOTIFICATION_SMTP_PASSWORD = settings.get( 'email.notification.smtp.password', ) self.EMAIL_REPLY_ACTIVATED = asbool(settings.get( 'email.reply.activated', False, )) self.EMAIL_REPLY_IMAP_SERVER = settings.get( 'email.reply.imap.server', ) self.EMAIL_REPLY_IMAP_PORT = settings.get( 'email.reply.imap.port', ) self.EMAIL_REPLY_IMAP_USER = settings.get( 'email.reply.imap.user', ) self.EMAIL_REPLY_IMAP_PASSWORD = settings.get( 'email.reply.imap.password', ) self.EMAIL_REPLY_IMAP_FOLDER = settings.get( 'email.reply.imap.folder', ) self.EMAIL_REPLY_CHECK_HEARTBEAT = int(settings.get( 'email.reply.check.heartbeat', 60, )) self.EMAIL_REPLY_IMAP_USE_SSL = asbool(settings.get( 'email.reply.imap.use_ssl', )) self.EMAIL_REPLY_IMAP_USE_IDLE = asbool(settings.get( 'email.reply.imap.use_idle', True, )) self.EMAIL_REPLY_CONNECTION_MAX_LIFETIME = int(settings.get( 'email.reply.connection.max_lifetime', 600, # 10 minutes )) self.EMAIL_REPLY_USE_HTML_PARSING = asbool(settings.get( 'email.reply.use_html_parsing', True, )) self.EMAIL_REPLY_USE_TXT_PARSING = asbool(settings.get( 'email.reply.use_txt_parsing', True, )) self.EMAIL_REPLY_LOCKFILE_PATH = settings.get( 'email.reply.lockfile_path', '' ) if not self.EMAIL_REPLY_LOCKFILE_PATH and self.EMAIL_REPLY_ACTIVATED: raise Exception( mandatory_msg.format('email.reply.lockfile_path') ) self.EMAIL_PROCESSING_MODE = settings.get( 'email.processing_mode', 'sync', ).upper() if self.EMAIL_PROCESSING_MODE not in ( self.CST.ASYNC, self.CST.SYNC, ): raise Exception( 'email.processing_mode ' 'can ''be "{}" or "{}", not "{}"'.format( self.CST.ASYNC, self.CST.SYNC, self.EMAIL_PROCESSING_MODE, ) ) self.EMAIL_SENDER_REDIS_HOST = settings.get( 'email.async.redis.host', 'localhost', ) self.EMAIL_SENDER_REDIS_PORT = int(settings.get( 'email.async.redis.port', 6379, )) self.EMAIL_SENDER_REDIS_DB = int(settings.get( 'email.async.redis.db', 0, )) self.NEW_USER_INVITATION_DO_NOTIFY = asbool(settings.get( 'new_user.invitation.do_notify', 'True' )) self.NEW_USER_INVITATION_MINIMAL_PROFILE = settings.get( 'new_user.invitation.minimal_profile', Group.TIM_MANAGER_GROUPNAME ) ### # WSGIDAV (Webdav server) ### tracim_website = 'http://tracim.fr/' tracim_name = 'Tracim' wsgidav_website = 'https://github.com/mar10/wsgidav/' wsgidav_name = 'WsgiDAV' self.WEBDAV_VERBOSE_LEVEL = int(settings.get('webdav.verbose.level', 1)) self.WEBDAV_ROOT_PATH = settings.get('webdav.root_path', '/') self.WEBDAV_BLOCK_SIZE = int(settings.get('webdav.block_size', 8192)) self.WEBDAV_DIR_BROWSER_ENABLED = asbool(settings.get('webdav.dir_browser.enabled', True)) default_webdav_footnote = '<a href="{instance_url}">{instance_name}</a>.' \ ' This Webdav is serve by' \ ' <a href="{tracim_website}">{tracim_name} software</a> using' \ ' <a href="{wsgidav_website}">{wsgidav_name}</a>.'.format( instance_name=self.WEBSITE_TITLE, instance_url=self.WEBSITE_BASE_URL, tracim_name=tracim_name, tracim_website=tracim_website, wsgidav_name=wsgidav_name, wsgidav_website=wsgidav_website, ) self.WEBDAV_DIR_BROWSER_FOOTER = settings.get('webdav.dir_browser.footer', default_webdav_footnote) # TODO : check if tweaking those param does work self.WEBDAV_SHOW_DELETED = False self.WEBDAV_SHOW_ARCHIVED = False self.WEBDAV_SHOW_HISTORY = False self.WEBDAV_MANAGE_LOCK = True # TODO - G.M - 27-03-2018 - [Caldav] Restore radicale config ### # RADICALE (Caldav server) ### # self.RADICALE_SERVER_HOST = settings.get( # 'radicale.server.host', # '127.0.0.1', # ) # self.RADICALE_SERVER_PORT = int(settings.get( # 'radicale.server.port', # 5232, # )) # # Note: Other parameters needed to work in SSL (cert file, etc) # self.RADICALE_SERVER_SSL = asbool(settings.get( # 'radicale.server.ssl', # False, # )) # self.RADICALE_SERVER_FILE_SYSTEM_FOLDER = settings.get( # 'radicale.server.filesystem.folder', # ) # if not self.RADICALE_SERVER_FILE_SYSTEM_FOLDER: # raise Exception( # mandatory_msg.format('radicale.server.filesystem.folder') # ) # self.RADICALE_SERVER_ALLOW_ORIGIN = settings.get( # 'radicale.server.allow_origin', # None, # ) # if not self.RADICALE_SERVER_ALLOW_ORIGIN: # self.RADICALE_SERVER_ALLOW_ORIGIN = self.WEBSITE_BASE_URL # logger.warning(self, # 'NOTE: Generated radicale.server.allow_origin parameter with ' # 'followings parameters: website.base_url ({0})' # .format(self.WEBSITE_BASE_URL) # ) # # self.RADICALE_SERVER_REALM_MESSAGE = settings.get( # 'radicale.server.realm_message', # 'Tracim Calendar - Password Required', # ) # # self.RADICALE_CLIENT_BASE_URL_HOST = settings.get( # 'radicale.client.base_url.host', # 'http://{}:{}'.format( # self.RADICALE_SERVER_HOST, # self.RADICALE_SERVER_PORT, # ), # ) # # self.RADICALE_CLIENT_BASE_URL_PREFIX = settings.get( # 'radicale.client.base_url.prefix', # '/', # ) # # Ensure finished by '/' # if '/' != self.RADICALE_CLIENT_BASE_URL_PREFIX[-1]: # self.RADICALE_CLIENT_BASE_URL_PREFIX += '/' # if '/' != self.RADICALE_CLIENT_BASE_URL_PREFIX[0]: # self.RADICALE_CLIENT_BASE_URL_PREFIX \ # = '/' + self.RADICALE_CLIENT_BASE_URL_PREFIX # # if not self.RADICALE_CLIENT_BASE_URL_HOST: # logger.warning(self, # 'Generated radicale.client.base_url.host parameter with ' # 'followings parameters: website.server_name -> {}' # .format(self.WEBSITE_SERVER_NAME) # ) # self.RADICALE_CLIENT_BASE_URL_HOST = self.WEBSITE_SERVER_NAME # # self.RADICALE_CLIENT_BASE_URL_TEMPLATE = '{}{}'.format( # self.RADICALE_CLIENT_BASE_URL_HOST, # self.RADICALE_CLIENT_BASE_URL_PREFIX, # ) self.PREVIEW_JPG_RESTRICTED_DIMS = asbool(settings.get( 'preview.jpg.restricted_dims', False )) preview_jpg_allowed_dims_str = settings.get('preview.jpg.allowed_dims', '') # nopep8 allowed_dims = [] if preview_jpg_allowed_dims_str: for sizes in preview_jpg_allowed_dims_str.split(','): parts = sizes.split('x') assert len(parts) == 2 width, height = parts assert width.isdecimal() assert height.isdecimal() size = PreviewDim(int(width), int(height)) allowed_dims.append(size) if not allowed_dims: size = PreviewDim(256, 256) allowed_dims.append(size) self.PREVIEW_JPG_ALLOWED_DIMS = allowed_dims self.FRONTEND_SERVE = asbool(settings.get( 'frontend.serve', False )) # INFO - G.M - 2018-08-06 - we pretend that frontend_dist_folder # is probably in frontend subfolder # of tracim_v2 parent of both backend and frontend backend_folder = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) # nopep8 tracim_v2_folder = os.path.dirname(backend_folder) backend_i18n_folder = os.path.join(backend_folder, 'tracim_backend', 'locale') # nopep8 self.BACKEND_I18N_FOLDER = settings.get( 'backend.18n_folder_path', backend_i18n_folder ) if not os.path.isdir(self.BACKEND_I18N_FOLDER): raise Exception( 'ERROR: {} folder does not exist as folder. ' 'please set backend.i8n_folder_path' 'with a correct value'.format(self.BACKEND_I18N_FOLDER) ) frontend_dist_folder = os.path.join(tracim_v2_folder, 'frontend', 'dist') # nopep8 self.FRONTEND_DIST_FOLDER_PATH = settings.get( 'frontend.dist_folder_path', frontend_dist_folder ) # INFO - G.M - 2018-08-06 - We check dist folder existence if self.FRONTEND_SERVE and not os.path.isdir(self.FRONTEND_DIST_FOLDER_PATH): # nopep8 raise Exception( 'ERROR: {} folder does not exist as folder. ' 'please set frontend.dist_folder.path' 'with a correct value'.format(self.FRONTEND_DIST_FOLDER_PATH) ) self.load_ldap_settings(settings)
def append_thread_callback(self, callback: typing.Callable) -> None: logger.warning( "MailFetcherrDaemon not implement append_thread_callback") pass
def __init__(self, settings): """Parse configuration file.""" ### # General ### backend_folder = os.path.dirname( os.path.dirname(os.path.abspath(__file__))) # nopep8 tracim_v2_folder = os.path.dirname(backend_folder) default_color_config_file_path = os.path.join(tracim_v2_folder, 'color.json') # nopep8 self.COLOR_CONFIG_FILE_PATH = settings.get( 'color.config_file_path', default_color_config_file_path) if not os.path.exists(self.COLOR_CONFIG_FILE_PATH): raise Exception('ERROR: {} file does not exist. ' 'please create it or set color.config_file_path' 'with a correct value'.format( self.COLOR_CONFIG_FILE_PATH)) try: with open(self.COLOR_CONFIG_FILE_PATH) as json_file: self.APPS_COLORS = json.load(json_file) except Exception as e: raise Exception('Error: {} file could not be load as json'.format( self.COLOR_CONFIG_FILE_PATH) # nopep8 ) from e try: self.APPS_COLORS['primary'] except KeyError as e: raise Exception( 'Error: primary color is required in {} file'.format( self.COLOR_CONFIG_FILE_PATH) # nopep8 ) from e default_enabled_app = [ 'contents/thread', 'contents/file', 'contents/html-document', ] enabled_app = [] enabled_app_str = settings.get('app.enabled', None) if enabled_app_str: for app in enabled_app_str.split(','): app_name = app.strip() enabled_app.append(app_name) else: enabled_app = default_enabled_app self.ENABLED_APP = enabled_app self._set_default_app(self.ENABLED_APP) mandatory_msg = \ 'ERROR: {} configuration is mandatory. Set it before continuing.' self.DEPOT_STORAGE_DIR = settings.get('depot_storage_dir', ) if not self.DEPOT_STORAGE_DIR: raise Exception(mandatory_msg.format('depot_storage_dir')) self.DEPOT_STORAGE_NAME = settings.get('depot_storage_name', ) if not self.DEPOT_STORAGE_NAME: raise Exception(mandatory_msg.format('depot_storage_name')) self.PREVIEW_CACHE_DIR = settings.get('preview_cache_dir', ) if not self.PREVIEW_CACHE_DIR: raise Exception( 'ERROR: preview_cache_dir configuration is mandatory. ' 'Set it before continuing.') # TODO - G.M - 2018-09-11 - Deprecated param # self.DATA_UPDATE_ALLOWED_DURATION = int(settings.get( # 'content.update.allowed.duration', # 0, # )) self.API_KEY = settings.get('api.key', '') self.SESSION_REISSUE_TIME = int( settings.get('session.reissue_time', 120)) self.WEBSITE_TITLE = settings.get( 'website.title', 'TRACIM', ) # base url of the frontend self.WEBSITE_BASE_URL = settings.get( 'website.base_url', '', ) if not self.WEBSITE_BASE_URL: raise Exception( 'website.base_url is needed in order to have correct path in' 'few place like in email.' 'You should set it with frontend root url.') self.API_BASE_URL = settings.get( 'api.base_url', self.WEBSITE_BASE_URL, ) allowed_origin = [] allowed_origin_string = settings.get( 'cors.access-control-allowed-origin', '') if allowed_origin_string: allowed_origin.extend(allowed_origin_string.split(',')) # nopep8 if not allowed_origin: allowed_origin.append(self.WEBSITE_BASE_URL) if self.API_BASE_URL != self.WEBSITE_BASE_URL: allowed_origin.append(self.API_BASE_URL) self.CORS_ALLOWED_ORIGIN = allowed_origin # TODO - G.M - 26-03-2018 - [Cleanup] These params seems deprecated for tracimv2, # nopep8 # Verify this # # self.WEBSITE_HOME_TITLE_COLOR = settings.get( # 'website.title.color', # '#555', # ) # self.WEBSITE_HOME_IMAGE_PATH = settings.get( # '/assets/img/home_illustration.jpg', # ) # self.WEBSITE_HOME_BACKGROUND_IMAGE_PATH = settings.get( # '/assets/img/bg.jpg', # ) # website_server_name = settings.get( 'website.server_name', None, ) if not website_server_name: website_server_name = urlparse(self.WEBSITE_BASE_URL).hostname logger.warning( self, 'NOTE: Generated website.server_name parameter from ' 'website.base_url parameter -> {0}'.format( website_server_name)) self.WEBSITE_SERVER_NAME = website_server_name # TODO - G.M - 2018-09-11 - Deprecated params # self.WEBSITE_HOME_TAG_LINE = settings.get( # 'website.home.tag_line', # '', # ) # self.WEBSITE_SUBTITLE = settings.get( # 'website.home.subtitle', # '', # ) # self.WEBSITE_HOME_BELOW_LOGIN_FORM = settings.get( # 'website.home.below_login_form', # '', # ) # # self.WEBSITE_TREEVIEW_CONTENT = settings.get( # 'website.treeview.content', # ) self.USER_AUTH_TOKEN_VALIDITY = int( settings.get( 'user.auth_token.validity', '604800', )) self.USER_RESET_PASSWORD_TOKEN_VALIDITY = int( settings.get('user.reset_password.validity', '900')) self.DEBUG = asbool(settings.get('debug', False)) # TODO - G.M - 27-03-2018 - [Email] Restore email config ### # EMAIL related stuff (notification, reply) ## self.EMAIL_NOTIFICATION_NOTIFIED_EVENTS = [ ActionDescription.COMMENT, ActionDescription.CREATION, ActionDescription.EDITION, ActionDescription.REVISION, ActionDescription.STATUS_UPDATE ] self.EMAIL_NOTIFICATION_NOTIFIED_CONTENTS = [ content_type_list.Page.slug, content_type_list.Thread.slug, content_type_list.File.slug, content_type_list.Comment.slug, # content_type_list.Folder.slug -- Folder is skipped ] if settings.get('email.notification.from'): raise Exception( 'email.notification.from configuration is deprecated. ' 'Use instead email.notification.from.email and ' 'email.notification.from.default_label.') self.EMAIL_NOTIFICATION_FROM_EMAIL = settings.get( 'email.notification.from.email', 'noreply+{user_id}@trac.im') self.EMAIL_NOTIFICATION_FROM_DEFAULT_LABEL = settings.get( 'email.notification.from.default_label', 'Tracim Notifications') self.EMAIL_NOTIFICATION_REPLY_TO_EMAIL = settings.get( 'email.notification.reply_to.email', ) self.EMAIL_NOTIFICATION_REFERENCES_EMAIL = settings.get( 'email.notification.references.email') # Content update notification self.EMAIL_NOTIFICATION_CONTENT_UPDATE_TEMPLATE_HTML = settings.get( 'email.notification.content_update.template.html', ) self.EMAIL_NOTIFICATION_CONTENT_UPDATE_TEMPLATE_TEXT = settings.get( 'email.notification.content_update.template.text', ) self.EMAIL_NOTIFICATION_CONTENT_UPDATE_SUBJECT = settings.get( 'email.notification.content_update.subject', ) # Created account notification self.EMAIL_NOTIFICATION_CREATED_ACCOUNT_TEMPLATE_HTML = settings.get( 'email.notification.created_account.template.html', './tracim_backend/templates/mail/created_account_body_html.mak', ) self.EMAIL_NOTIFICATION_CREATED_ACCOUNT_TEMPLATE_TEXT = settings.get( 'email.notification.created_account.template.text', './tracim_backend/templates/mail/created_account_body_text.mak', ) self.EMAIL_NOTIFICATION_CREATED_ACCOUNT_SUBJECT = settings.get( 'email.notification.created_account.subject', '[{website_title}] Created account', ) # Reset password notification self.EMAIL_NOTIFICATION_RESET_PASSWORD_TEMPLATE_HTML = settings.get( 'email.notification.reset_password_request.template.html', './tracim_backend/templates/mail/reset_password_body_html.mak', ) self.EMAIL_NOTIFICATION_RESET_PASSWORD_TEMPLATE_TEXT = settings.get( 'email.notification.reset_password_request.template.text', './tracim_backend/templates/mail/reset_password_body_text.mak', ) self.EMAIL_NOTIFICATION_RESET_PASSWORD_SUBJECT = settings.get( 'email.notification.reset_password_request.subject', '[{website_title}] Reset Password Request') self.EMAIL_NOTIFICATION_PROCESSING_MODE = settings.get( 'email.notification.processing_mode', ) self.EMAIL_NOTIFICATION_ACTIVATED = asbool( settings.get('email.notification.activated', )) if not self.EMAIL_NOTIFICATION_ACTIVATED: logger.warning( self, 'Notification by email mecanism is disabled ! ' 'Notification and mail invitation mecanisms will not work.') self.EMAIL_NOTIFICATION_SMTP_SERVER = settings.get( 'email.notification.smtp.server', ) self.EMAIL_NOTIFICATION_SMTP_PORT = settings.get( 'email.notification.smtp.port', ) self.EMAIL_NOTIFICATION_SMTP_USER = settings.get( 'email.notification.smtp.user', ) self.EMAIL_NOTIFICATION_SMTP_PASSWORD = settings.get( 'email.notification.smtp.password', ) self.EMAIL_NOTIFICATION_LOG_FILE_PATH = settings.get( 'email.notification.log_file_path', None, ) self.EMAIL_REPLY_ACTIVATED = asbool( settings.get( 'email.reply.activated', False, )) self.EMAIL_REPLY_IMAP_SERVER = settings.get( 'email.reply.imap.server', ) self.EMAIL_REPLY_IMAP_PORT = settings.get('email.reply.imap.port', ) self.EMAIL_REPLY_IMAP_USER = settings.get('email.reply.imap.user', ) self.EMAIL_REPLY_IMAP_PASSWORD = settings.get( 'email.reply.imap.password', ) self.EMAIL_REPLY_IMAP_FOLDER = settings.get( 'email.reply.imap.folder', ) self.EMAIL_REPLY_CHECK_HEARTBEAT = int( settings.get( 'email.reply.check.heartbeat', 60, )) self.EMAIL_REPLY_TOKEN = settings.get('email.reply.token', ) self.EMAIL_REPLY_IMAP_USE_SSL = asbool( settings.get('email.reply.imap.use_ssl', )) self.EMAIL_REPLY_IMAP_USE_IDLE = asbool( settings.get( 'email.reply.imap.use_idle', True, )) self.EMAIL_REPLY_CONNECTION_MAX_LIFETIME = int( settings.get( 'email.reply.connection.max_lifetime', 600, # 10 minutes )) self.EMAIL_REPLY_USE_HTML_PARSING = asbool( settings.get( 'email.reply.use_html_parsing', True, )) self.EMAIL_REPLY_USE_TXT_PARSING = asbool( settings.get( 'email.reply.use_txt_parsing', True, )) self.EMAIL_REPLY_LOCKFILE_PATH = settings.get( 'email.reply.lockfile_path', '') if not self.EMAIL_REPLY_LOCKFILE_PATH and self.EMAIL_REPLY_ACTIVATED: raise Exception(mandatory_msg.format('email.reply.lockfile_path')) self.EMAIL_PROCESSING_MODE = settings.get( 'email.processing_mode', 'sync', ).upper() if self.EMAIL_PROCESSING_MODE not in ( self.CST.ASYNC, self.CST.SYNC, ): raise Exception('email.processing_mode ' 'can ' 'be "{}" or "{}", not "{}"'.format( self.CST.ASYNC, self.CST.SYNC, self.EMAIL_PROCESSING_MODE, )) self.EMAIL_SENDER_REDIS_HOST = settings.get( 'email.async.redis.host', 'localhost', ) self.EMAIL_SENDER_REDIS_PORT = int( settings.get( 'email.async.redis.port', 6379, )) self.EMAIL_SENDER_REDIS_DB = int( settings.get( 'email.async.redis.db', 0, )) self.INVITE_NEW_USER_MINIMAL_PROFILE = settings.get( 'invitation.new_user.minimal_profile', Group.TIM_MANAGER_GROUPNAME) ### # WSGIDAV (Webdav server) ### # TODO - G.M - 27-03-2018 - [WebDav] Restore wsgidav config #self.WSGIDAV_CONFIG_PATH = settings.get( # 'wsgidav.config_path', # 'wsgidav.conf', #) # TODO: Convert to importlib # http://stackoverflow.com/questions/41063938/use-importlib-instead-imp-for-non-py-file #self.wsgidav_config = imp.load_source( # 'wsgidav_config', # self.WSGIDAV_CONFIG_PATH, #) # self.WSGIDAV_PORT = self.wsgidav_config.port # self.WSGIDAV_CLIENT_BASE_URL = settings.get( # 'wsgidav.client.base_url', # None, # ) # # if not self.WSGIDAV_CLIENT_BASE_URL: # self.WSGIDAV_CLIENT_BASE_URL = \ # '{0}:{1}'.format( # self.WEBSITE_SERVER_NAME, # self.WSGIDAV_PORT, # ) # logger.warning(self, # 'NOTE: Generated wsgidav.client.base_url parameter with ' # 'followings parameters: website.server_name and ' # 'wsgidav.conf port'.format( # self.WSGIDAV_CLIENT_BASE_URL, # ) # ) # # if not self.WSGIDAV_CLIENT_BASE_URL.endswith('/'): # self.WSGIDAV_CLIENT_BASE_URL += '/' # TODO - G.M - 27-03-2018 - [Caldav] Restore radicale config ### # RADICALE (Caldav server) ### # self.RADICALE_SERVER_HOST = settings.get( # 'radicale.server.host', # '127.0.0.1', # ) # self.RADICALE_SERVER_PORT = int(settings.get( # 'radicale.server.port', # 5232, # )) # # Note: Other parameters needed to work in SSL (cert file, etc) # self.RADICALE_SERVER_SSL = asbool(settings.get( # 'radicale.server.ssl', # False, # )) # self.RADICALE_SERVER_FILE_SYSTEM_FOLDER = settings.get( # 'radicale.server.filesystem.folder', # ) # if not self.RADICALE_SERVER_FILE_SYSTEM_FOLDER: # raise Exception( # mandatory_msg.format('radicale.server.filesystem.folder') # ) # self.RADICALE_SERVER_ALLOW_ORIGIN = settings.get( # 'radicale.server.allow_origin', # None, # ) # if not self.RADICALE_SERVER_ALLOW_ORIGIN: # self.RADICALE_SERVER_ALLOW_ORIGIN = self.WEBSITE_BASE_URL # logger.warning(self, # 'NOTE: Generated radicale.server.allow_origin parameter with ' # 'followings parameters: website.base_url ({0})' # .format(self.WEBSITE_BASE_URL) # ) # # self.RADICALE_SERVER_REALM_MESSAGE = settings.get( # 'radicale.server.realm_message', # 'Tracim Calendar - Password Required', # ) # # self.RADICALE_CLIENT_BASE_URL_HOST = settings.get( # 'radicale.client.base_url.host', # 'http://{}:{}'.format( # self.RADICALE_SERVER_HOST, # self.RADICALE_SERVER_PORT, # ), # ) # # self.RADICALE_CLIENT_BASE_URL_PREFIX = settings.get( # 'radicale.client.base_url.prefix', # '/', # ) # # Ensure finished by '/' # if '/' != self.RADICALE_CLIENT_BASE_URL_PREFIX[-1]: # self.RADICALE_CLIENT_BASE_URL_PREFIX += '/' # if '/' != self.RADICALE_CLIENT_BASE_URL_PREFIX[0]: # self.RADICALE_CLIENT_BASE_URL_PREFIX \ # = '/' + self.RADICALE_CLIENT_BASE_URL_PREFIX # # if not self.RADICALE_CLIENT_BASE_URL_HOST: # logger.warning(self, # 'Generated radicale.client.base_url.host parameter with ' # 'followings parameters: website.server_name -> {}' # .format(self.WEBSITE_SERVER_NAME) # ) # self.RADICALE_CLIENT_BASE_URL_HOST = self.WEBSITE_SERVER_NAME # # self.RADICALE_CLIENT_BASE_URL_TEMPLATE = '{}{}'.format( # self.RADICALE_CLIENT_BASE_URL_HOST, # self.RADICALE_CLIENT_BASE_URL_PREFIX, # ) self.PREVIEW_JPG_RESTRICTED_DIMS = asbool( settings.get('preview.jpg.restricted_dims', False)) preview_jpg_allowed_dims_str = settings.get('preview.jpg.allowed_dims', '') # nopep8 allowed_dims = [] if preview_jpg_allowed_dims_str: for sizes in preview_jpg_allowed_dims_str.split(','): parts = sizes.split('x') assert len(parts) == 2 width, height = parts assert width.isdecimal() assert height.isdecimal() size = PreviewDim(int(width), int(height)) allowed_dims.append(size) if not allowed_dims: size = PreviewDim(256, 256) allowed_dims.append(size) self.PREVIEW_JPG_ALLOWED_DIMS = allowed_dims self.FRONTEND_SERVE = asbool(settings.get('frontend.serve', False)) # INFO - G.M - 2018-08-06 - we pretend that frontend_dist_folder # is probably in frontend subfolder # of tracim_v2 parent of both backend and frontend backend_folder = os.path.dirname( os.path.dirname(os.path.abspath(__file__))) # nopep8 tracim_v2_folder = os.path.dirname(backend_folder) backend_i18n_folder = os.path.join(backend_folder, 'tracim_backend', 'locale') # nopep8 self.BACKEND_I18N_FOLDER = settings.get('backend.18n_folder_path', backend_i18n_folder) if not os.path.isdir(self.BACKEND_I18N_FOLDER): raise Exception('ERROR: {} folder does not exist as folder. ' 'please set backend.i8n_folder_path' 'with a correct value'.format( self.BACKEND_I18N_FOLDER)) frontend_dist_folder = os.path.join(tracim_v2_folder, 'frontend', 'dist') # nopep8 self.FRONTEND_DIST_FOLDER_PATH = settings.get( 'frontend.dist_folder_path', frontend_dist_folder) # INFO - G.M - 2018-08-06 - We check dist folder existence if self.FRONTEND_SERVE and not os.path.isdir( self.FRONTEND_DIST_FOLDER_PATH): # nopep8 raise Exception('ERROR: {} folder does not exist as folder. ' 'please set frontend.dist_folder.path' 'with a correct value'.format( self.FRONTEND_DIST_FOLDER_PATH))
def notify_content_update(self, content: Content): if content.get_last_action( ).id not in self.config.EMAIL__NOTIFICATION__NOTIFIED_EVENTS: logger.info( self, "Skip email notification for update of content {}" "by user {} (the action is {})".format( content.content_id, # below: 0 means "no user" self._user.user_id if self._user else 0, content.get_last_action().id, ), ) return logger.info( self, "About to email-notify update" "of content {} by user {}".format( content.content_id, # Below: 0 means "no user" self._user.user_id if self._user else 0, ), ) if content.type not in self.config.EMAIL__NOTIFICATION__NOTIFIED_CONTENTS: logger.info( self, "Skip email notification for update of content {}" "by user {} (the content type is {})".format( content.type, # below: 0 means "no user" self._user.user_id if self._user else 0, content.get_last_action().id, ), ) return logger.info( self, "About to email-notify update" "of content {} by user {}".format( content.content_id, # Below: 0 means "no user" self._user.user_id if self._user else 0, ), ) #### # # INFO - D.A. - 2014-11-05 - Emails are sent through asynchronous jobs. # For that reason, we do not give SQLAlchemy objects but ids only # (SQLA objects are related to a given thread/session) # try: # TODO - D.A - 2014-11-06 # This feature must be implemented in order to be able to scale to large communities if self.config.JOBS__PROCESSING_MODE == self.config.CST.ASYNC: logger.warning( self, "Creating mails in SYNC mode as ASYNC is not supported yet." ) else: logger.info(self, "Creating email in SYNC mode") EmailManager(self._smtp_config, self.config, self.session).notify_content_update( self._user.user_id, content.content_id) except Exception: logger.exception(self, "Exception catched during email notification")
def _load_global_config(self) -> None: """ Load generic config """ ### # General ### self.SQLALCHEMY__URL = self.get_raw_config("sqlalchemy.url", "") self.DEFAULT_LANG = self.get_raw_config("default_lang", DEFAULT_FALLBACK_LANG) backend_folder = os.path.dirname( os.path.dirname(os.path.abspath(__file__))) tracim_v2_folder = os.path.dirname(backend_folder) default_color_config_file_path = os.path.join(tracim_v2_folder, "color.json") self.COLOR__CONFIG_FILE_PATH = self.get_raw_config( "color.config_file_path", default_color_config_file_path) default_enabled_app = ("contents/thread," "contents/file," "contents/html-document," "contents/folder," "agenda") self.APP__ENABLED = string_to_list( self.get_raw_config("app.enabled", default_enabled_app), separator=",", cast_func=str, do_strip=True, ) self.DEPOT_STORAGE_DIR = self.get_raw_config("depot_storage_dir") self.DEPOT_STORAGE_NAME = self.get_raw_config("depot_storage_name") self.PREVIEW_CACHE_DIR = self.get_raw_config("preview_cache_dir") self.AUTH_TYPES = string_to_list( self.get_raw_config("auth_types", "internal"), separator=",", cast_func=AuthType, do_strip=True, ) self.REMOTE_USER_HEADER = self.get_raw_config("remote_user_header", None) # TODO - G.M - 2018-09-11 - Deprecated param # self.DATA_UPDATE_ALLOWED_DURATION = int(self.get_raw_config( # 'content.update.allowed.duration', # 0, # )) self.API__KEY = self.get_raw_config("api.key", "", secret=True) self.SESSION__REISSUE_TIME = int( self.get_raw_config("session.reissue_time", "120")) self.SESSION__DATA_DIR = self.get_raw_config("session.data_dir") self.SESSION__LOCK_DIR = self.get_raw_config("session.lock_dir") self.WEBSITE__TITLE = self.get_raw_config("website.title", "TRACIM") # base url of the frontend self.WEBSITE__BASE_URL = self.get_raw_config("website.base_url", "") self.API__BASE_URL = self.get_raw_config("api.base_url", self.WEBSITE__BASE_URL) if self.API__BASE_URL != self.WEBSITE__BASE_URL: default_cors_allowed_origin = "{},{}".format( self.WEBSITE__BASE_URL, self.API__BASE_URL) else: default_cors_allowed_origin = self.WEBSITE__BASE_URL self.CORS__ACCESS_CONTROL_ALLOWED_ORIGIN = string_to_list( self.get_raw_config("cors.access-control-allowed-origin", default_cors_allowed_origin), separator=",", cast_func=str, do_strip=True, ) self.USER__AUTH_TOKEN__VALIDITY = int( self.get_raw_config("user.auth_token.validity", "604800")) # TODO - G.M - 2019-03-14 - retrocompat code, # will be deleted in the future (https://github.com/tracim/tracim/issues/1483) defaut_reset_password_validity = "900" self.USER__RESET_PASSWORD__VALIDITY = self.get_raw_config( "user.reset_password.validity") if self.USER__RESET_PASSWORD__VALIDITY: logger.warning( self, "user.reset_password.validity parameter is deprecated ! " "please use user.reset_password.token_lifetime instead.", ) self.USER__RESET_PASSWORD__TOKEN_LIFETIME = self.USER__RESET_PASSWORD__VALIDITY else: self.USER__RESET_PASSWORD__TOKEN_LIFETIME = int( self.get_raw_config("user.reset_password.token_lifetime", defaut_reset_password_validity)) self.DEBUG = asbool(self.get_raw_config("debug", "false")) self.PREVIEW__JPG__RESTRICTED_DIMS = asbool( self.get_raw_config("preview.jpg.restricted_dims", "false")) self.PREVIEW__JPG__ALLOWED_DIMS = string_to_list( self.get_raw_config("preview.jpg.allowed_dims", "256x256"), cast_func=PreviewDim.from_string, separator=",", ) self.FRONTEND__SERVE = asbool( self.get_raw_config("frontend.serve", "false")) # INFO - G.M - 2018-08-06 - we pretend that frontend_dist_folder # is probably in frontend subfolder # of tracim_v2 parent of both backend and frontend backend_folder = os.path.dirname( os.path.dirname(os.path.abspath(__file__))) tracim_v2_folder = os.path.dirname(backend_folder) backend_i18n_folder = os.path.join(backend_folder, "tracim_backend", "locale") self.BACKEND__I18N_FOLDER_PATH = self.get_raw_config( "backend.i18n_folder_path", backend_i18n_folder) frontend_dist_folder = os.path.join(tracim_v2_folder, "frontend", "dist") self.FRONTEND__DIST_FOLDER_PATH = self.get_raw_config( "frontend.dist_folder_path", frontend_dist_folder)
def _check_email_config_validity(self) -> None: """ Check if config is correctly setted for email features """ if not self.EMAIL__NOTIFICATION__ACTIVATED: logger.warning( self, "Notification by email mechanism is disabled! " "Notification and mail invitation mechanisms will not work.", ) if not self.EMAIL__REPLY__LOCKFILE_PATH and self.EMAIL__REPLY__ACTIVATED: self.check_mandatory_param( "EMAIL__REPLY__LOCKFILE_PATH", self.EMAIL__REPLY__LOCKFILE_PATH, when_str="when email reply is activated", ) if self.EMAIL__REPLY__ACTIVATED: # INFO - G.M - 2019-12-10 - check imap config provided self.check_mandatory_param( "EMAIL__REPLY__IMAP__SERVER", self.EMAIL__REPLY__IMAP__SERVER, when_str="when email notification is activated", ) self.check_mandatory_param( "EMAIL__REPLY__IMAP__PORT", self.EMAIL__REPLY__IMAP__PORT, when_str="when email notification is activated", ) self.check_mandatory_param( "EMAIL__REPLY__IMAP__USER", self.EMAIL__REPLY__IMAP__USER, when_str="when email notification is activated", ) self.check_mandatory_param( "EMAIL__REPLY__IMAP__PASSWORD", self.EMAIL__REPLY__IMAP__PASSWORD, when_str="when email notification is activated", ) self.check_mandatory_param( "EMAIL__REPLY__IMAP__FOLDER", self.EMAIL__REPLY__IMAP__FOLDER, when_str="when email notification is activated", ) if self.EMAIL__NOTIFICATION__ACTIVATED: # INFO - G.M - 2019-12-10 - check smtp config provided self.check_mandatory_param( "EMAIL__NOTIFICATION__SMTP__SERVER", self.EMAIL__NOTIFICATION__SMTP__SERVER, when_str="when email notification is activated", ) self.check_mandatory_param( "EMAIL__NOTIFICATION__SMTP__PORT", self.EMAIL__NOTIFICATION__SMTP__PORT, when_str="when email notification is activated", ) self.check_mandatory_param( "EMAIL__NOTIFICATION__SMTP__USER", self.EMAIL__NOTIFICATION__SMTP__USER, when_str="when email notification is activated", ) self.check_mandatory_param( "EMAIL__NOTIFICATION__SMTP__PASSWORD", self.EMAIL__NOTIFICATION__SMTP__PASSWORD, when_str="when email notification is activated", ) # INFO - G.M - 2019-12-10 - check value provided for headers self.check_mandatory_param( "EMAIL__NOTIFICATION__FROM__EMAIL", self.EMAIL__NOTIFICATION__FROM__EMAIL, when_str="when email notification is activated", ) self.check_mandatory_param( "EMAIL__NOTIFICATION__FROM__DEFAULT_LABEL", self.EMAIL__NOTIFICATION__FROM__DEFAULT_LABEL, when_str="when email notification is activated", ) self.check_mandatory_param( "EMAIL__NOTIFICATION__REPLY_TO__EMAIL", self.EMAIL__NOTIFICATION__REPLY_TO__EMAIL, when_str="when email notification is activated", ) self.check_mandatory_param( "EMAIL__NOTIFICATION__REFERENCES__EMAIL", self.EMAIL__NOTIFICATION__REFERENCES__EMAIL, when_str="when email notification is activated", ) # INFO - G.M - 2019-02-01 - check if template are available, # do not allow running with email_notification_activated # if templates needed are not available templates = { "content_update notification": self.EMAIL__NOTIFICATION__CONTENT_UPDATE__TEMPLATE__HTML, "created account": self.EMAIL__NOTIFICATION__CREATED_ACCOUNT__TEMPLATE__HTML, "password reset": self. EMAIL__NOTIFICATION__RESET_PASSWORD_REQUEST__TEMPLATE__HTML, } for template_description, template_path in templates.items(): if not template_path or not os.path.isfile(template_path): raise ConfigurationError( "ERROR: email template for {template_description} " 'not found at "{template_path}."'.format( template_description=template_description, template_path=template_path, ))