|
+ Ahoi, +ein Schiffsanlauf benötigt Ihre Aufmerksamkeit. Bei der Prüfung der Daten haben wir wahrgenommen, dass ein Problem aufgetreten sein könnte. +
#ADAPTIVECONTENT +
|
+
diff --git a/src/server/BreCal/database/sql_handler.py b/src/server/BreCal/database/sql_handler.py index 2d85175..05c9296 100644 --- a/src/server/BreCal/database/sql_handler.py +++ b/src/server/BreCal/database/sql_handler.py @@ -33,7 +33,8 @@ def get_synchronous_shipcall_times_standalone(query_time:pd.Timestamp, all_df_ti returns: counts """ - assert isinstance(query_time,pd.Timestamp) + assert isinstance(query_time,pd.Timestamp) or pd.isnull(query_time), f"expected query_time to be a pd.Timestamp or pd.NaT. Found: {type(query_time)}" + assert isinstance(all_df_times,pd.DataFrame) # get a timedelta for each valid (not Null) time entry time_deltas_eta = [(query_time.to_pydatetime()-time_.to_pydatetime()) for time_ in all_df_times.loc[:,"eta_berth"] if not pd.isnull(time_)] @@ -439,4 +440,5 @@ class SQLHandler(): def count_synchronous_shipcall_times(self, query_time:pd.Timestamp, all_df_times:pd.DataFrame, delta_threshold=900)->int: """count all times entries, which are too close to the query_time. The {delta_threshold} determines the threshold. returns counts (int)""" + assert isinstance(all_df_times, pd.DataFrame) return get_synchronous_shipcall_times_standalone(query_time, all_df_times, delta_threshold) diff --git a/src/server/BreCal/database/sql_queries.py b/src/server/BreCal/database/sql_queries.py index 147ee02..244c62f 100644 --- a/src/server/BreCal/database/sql_queries.py +++ b/src/server/BreCal/database/sql_queries.py @@ -222,7 +222,7 @@ class SQLQuery(): "api_key, notify_email, notify_whatsapp, notify_signal, notify_popup, created, modified FROM user " +\ "WHERE user_name = ?username? OR user_email = ?username?" return query - + @staticmethod def get_notifications()->str: query = "SELECT id, shipcall_id, level, type, message, created, modified FROM notification " + \ @@ -266,6 +266,11 @@ class SQLQuery(): query = "SELECT id, name, imo, callsign, participant_id, length, width, is_tug, bollard_pull, eni, created, modified, deleted FROM ship ORDER BY name" return query + @staticmethod + def get_ship_by_id()->str: + query = "SELECT id, name, imo, callsign, participant_id, length, width, is_tug, bollard_pull, eni, created, modified, deleted FROM ship WHERE id = ?id?" + return query + @staticmethod def get_times()->str: query = "SELECT id, eta_berth, eta_berth_fixed, etd_berth, etd_berth_fixed, lock_time, lock_time_fixed, " + \ @@ -321,6 +326,22 @@ class SQLQuery(): query = prefix+stage1+bridge+stage2+suffix return query + + @staticmethod + def get_notifications_post(schemaModel:dict)->str: + param_keys = {key:key for key in schemaModel.keys()} + + prefix = "INSERT INTO notification (" + bridge = ") VALUES (" + suffix = ")" + + non_dynamic_keys = ["id", "created", "modified"] + + stage1 = ",".join([key for key in schemaModel.keys() if not key in non_dynamic_keys]) + stage2 = ",".join([f"?{param_keys.get(key)}?" for key in schemaModel.keys() if not key in non_dynamic_keys]) + + query = prefix+stage1+bridge+stage2+suffix + return query @staticmethod def get_last_insert_id()->str: diff --git a/src/server/BreCal/notifications/accounts.py b/src/server/BreCal/notifications/accounts.py new file mode 100644 index 0000000..0e3bbd0 --- /dev/null +++ b/src/server/BreCal/notifications/accounts.py @@ -0,0 +1,7 @@ +"""This file contains login information to register into distinct notification accounts.""" + + +mail_server = 'w01d5503.kasserver.com' +mail_port=465 +mail_address="max.metz@scope-sorting.com" +mail_pwd = b'gAAAAABmqJlkXbtJTL1tFiyQNHhF_Y7sgtVI0xEx07ybwbX70Ro1Vp73CLDq49eFSYG-1SswIDQ2JBSORYlWaR-Vh2kIwPHy_lX8SxkySrRvBRzkyZP5x0I=' diff --git a/src/server/BreCal/notifications/notifier.py b/src/server/BreCal/notifications/notifier.py index dc74b91..9968fc6 100644 --- a/src/server/BreCal/notifications/notifier.py +++ b/src/server/BreCal/notifications/notifier.py @@ -1,7 +1,19 @@ import typing +import datetime from BreCal.database.sql_handler import execute_sql_query_standalone from BreCal.database.sql_queries import SQLQuery from BreCal.schemas import model +from BreCal.brecal_utils.time_handling import difference_to_then +from BreCal.notifications.accounts import mail_server, mail_port, mail_address, mail_pwd + +from BreCal.services.email_handling import EmailHandler, create_shipcall_evaluation_notification, send_notification, get_default_html_email +from BreCal.database.enums import ParticipantwiseTimeDelta + +eta_etd_type_dict = { + model.ShipcallType.arrival : "Ankunft", + model.ShipcallType.departure : "Abfahrt", + model.ShipcallType.shifting : "Wechselnd" +} class Notifier(): """ @@ -35,112 +47,82 @@ class Notifier(): - iterate over each remaining shipcall and apply .send_notification - those which are unsent, shall be sent by the respective type """ - raise NotImplementedError("skeleton") + # set a threshold, when alarm event notifications become eligible + time_diff_threshold = float(ParticipantwiseTimeDelta.NOTIFICATION)*60 # m minutes, converted to seconds + + debug = is_test # if is_test, the Emails will not be issued. Only a print message will be created. + update_database = True if not is_test else False # if_test, the database will not be updated. + time_diff_threshold = time_diff_threshold if not is_test else 0.0 # 0.0 delay when is_test is set. - # get all shipcalls - all_shipcalls = NotImplementedError + email_handler = EmailHandler(mail_server=mail_server, mail_port=mail_port, mail_address=mail_address) - shipcalls = [shipcall for shipcall in all_shipcalls if not shipcall.evaluation_notifications_sent] - for shipcall in shipcalls: - notification_list = Notifier.send_notification(shipcall, is_test=is_test) + # get candidates: find all eligible shipcalls, where the evaluation state is yellow or red & the notifications are not yet sent + eligible_shipcalls = Notifier.get_eligible_shipcalls(time_diff_threshold) - # #TODO: get all notifications - # #TODO: get matching shipcall (based on shipcall_id) + # get a list of tuples, where (shipcall, users) are combined for easier iterations. + notification_instructions = Notifier.build_notification_tuples(eligible_shipcalls) - # #TODO: filter: consider only those, which are not yet sent + if len(notification_instructions)>0: # only perform a login when there are any notification_instructions + try: + # login in advance, so the email handler uses a shared connection. It disconnects only once at the end of the call. + email_handler.login(interactive=False, pwd=mail_pwd) + + for shipcall, users in notification_instructions: + assert isinstance(shipcall, model.Shipcall) + assert isinstance(users,list) + assert all([isinstance(user,model.User) for user in users]) - # identify necessity - # #TODO: get the 'evaluation_notifications_sent' field from all shipcalls (based on shipcall_id) - # if not -> return - # USE shipcall.evaluation_notifications_sent + # iterate over each notification type + for notification_type in Notifier.get_all_notification_types(): + # consider only those users, which have subscribed to the respective notification type + eligible_users = Notifier.get_eligible_users(users, notification_type) - # #TODO: those which are unsent, shall be created&sent by the respective type -- Note: consider the is_test argument - # iterate over the list of Notifier.build_notification_type_list - # one might use Notifier.create(..., update_database=True) - # use the History (GetHistory -- by shipcall_id) to identify all subscribed users + if len(eligible_users)>0: + Notifier.create_and_send_notification_mapper(notification_type, shipcall, eligible_users, email_handler, mail_pwd, debug=debug) - # #TODO: update the shipcall dataset ('evaluation_notifications_sent') -- Note: consider the is_test argument + if not debug: + # populate notifications within the database to keep track + Notifier.generate_notification(shipcall, notification_type) - # #TODO_clarify: how to handle the 'evaluation_notifications_sent', when there is no recipient? + # update the database entries, so notifications are only sent once. + if update_database: + Notifier.shipcall_put_update_evaluation_notifications_sent_flag(shipcall) + + # #TODO: except... create log? + + finally: + email_handler.close() return + + @staticmethod + def build_notification_tuples(eligible_shipcalls)->list[typing.Tuple[model.Shipcall,list[model.User]]]: + """ + creates tuples, where shipcall and the list of attached users are grouped. One can iterate over the tuples + to perform actions, such as issueing notifications + """ + notification_instructions = [] + for shipcall in eligible_shipcalls: + # get all users, which are attached to the shipcall (uses the History dataset) + users = Notifier.get_users_via_history(shipcall.id) + + # create and store tuples of (shipcall, users) for each eligible shipcall + notification_instructions.append((shipcall, users)) + return notification_instructions @staticmethod - def send_notification(shipcall:model.Shipcall, is_test:bool=False)->list[model.Notification]: - """ - Complex-function, which is responsible of creating notification messages, issuing them to users and optionally updating - the database. The requirement is, that the notification is required and passes through an internal set of filters. + def get_eligible_users(users, notification_type): + eligible_users = [user for user in users if Notifier.check_user_is_subscribed_to_notification_type(user,notification_type=notification_type)] - Steps: - - get all notifications of shipcall_id - - identify the assigned list of users - - apply all filters. When a filter triggers, exit. If not, create and send a notification. - """ - update_database = False if is_test else True - # #TODO: the concept of old state and new state must be refactored. - # old state: read shipcall_id from notifications and look for the latest finding (if None -> EvaluationType.undefined) - # new state: read shipcall_id from shipcalls and look for the *current* 'evaluation' (-> EvaluationType(value)) - - # get existing notifications by shipcall_id (list) - existing_notifications = Notifier.get_existing_notifications(shipcall_id=shipcall.id) - old_state = NotImplementedError - - new_state = shipcall.evaluation - - # get User by querying all History objects of a shipcall_id - users = Notifier.get_users_via_history(shipcall_id=shipcall.id) - - # identify necessity - # state-check: Did the 'evaluation' shift to a higher level of severity? - severity_bool = Notifier.check_higher_severity(old_state, new_state) - if not severity_bool: - return None - - - # #TODO: time-based filter. There shall be 'enough' time between the evaluation time and NOW - evaluation_time = shipcall.evaluation_time - # latency_bool = #TODO_DIFFERENCE_FROM_NOW_TO_EVALUATION_TIME____THIS_METHOD_ALREADY_EXISTS(evaluation_time) - # careful: what is True, what is False? - # if latency_booL: - # return None - - notification_list = [] - for user in users: - notification = Notifier.create( - shipcall.id, - old_state, - new_state, - user, - update_database=update_database, - is_test=is_test - ) - notification_list.append(notification) - return notification_list - - @staticmethod - def publish(shipcall_id, old_state, new_state, user, update_database:bool=False)->typing.Optional[model.Notification]: - """ - Complex-function, which creates, sends and documents a notification. It serves as a convenience function. - The method does not apply internal filters to identify, whether a notification should be created in the first place. - - options: - update_database: bool. - # #TODO: instead of update_database, one may also use is_test - """ - # 1.) create - # ... = Notifier.create(shipcall_id, old_state, new_state, user) # e.g., might return a dictionary of dict[model.NotificationType, str], where str is the message - - # 2.) send - # ... = Notifier.send(...) # should contain internal 'logistics', which user the respective handlers to send notifications - - # 3.) document (mysql database) - # if update_database - # ... = Notifier.document(...) - raise NotImplementedError("skeleton") - return + # filter: consider only those users, where an Email is set + # #TODO: this is Email-specific and should not be a filter for other notifications + eligible_users = [user for user in eligible_users if user.user_email is not None] + return eligible_users @staticmethod def create(shipcall_id, old_state, new_state, user, update_database:bool=False)->typing.Optional[model.Notification]: """ + # #TODO_refactor: drastically change this method. It should only generate notifications, but not send them. + Standalone function, which creates a Notification for a specific user. Steps: @@ -173,6 +155,7 @@ class Notifier(): # get a list of all subscribed notification types and track the state (success or failure) + raise NotImplementedError("skeleton") successes = {} notification_type_list = Notifier.build_notification_type_list(user) for notification_type in notification_type_list: @@ -183,7 +166,6 @@ class Notifier(): success_state = Notifier.send_notification_by_type(notification_type, message) successes[notification_type] = success_state - raise NotImplementedError("skeleton") notification = ... return notification @@ -197,11 +179,10 @@ class Notifier(): def get_users_via_history(shipcall_id:int)->list[model.User]: """using the History objects, one can infer the user_id, which allows querying the Users""" histories = execute_sql_query_standalone(query=SQLQuery.get_history(), param={"shipcallid" : shipcall_id}, model=model.History, command_type="query") - user_ids = [ - history.user_id - for history in histories - ] - users = [Notifier.get_user(user_id) for user_id in user_ids] + assert isinstance(histories,list) + assert all([isinstance(history,model.History) for history in histories]) + + users = [Notifier.get_user(history.user_id) for history in histories] return users @staticmethod @@ -306,8 +287,249 @@ class Notifier(): else: raise ValueError(notification_type) return + + @staticmethod + def get_eligible_shipcalls(time_diff_threshold:float): + """ + get all eligible shipcalls, which do not have a sent notification yet + criterion a) notification shall not be sent yet (evaluation_notifications_sent = 0) + criterion b) evaluation state is yellow or red (type 2 or 3) + """ + query = 'SELECT * FROM shipcall WHERE (evaluation_notifications_sent = ?evaluation_notifications_sent?) AND (evaluation = 2 OR evaluation = 3)' + evaluation_notifications_sent = 0 + + # filter out the shipcalls, where the notifications were already sent (ignores null values, which is as expected) + eligible_shipcalls = execute_sql_query_standalone(query=query, model=model.Shipcall, param={"evaluation_notifications_sent" : evaluation_notifications_sent}) + + # filter out the shipcalls, where evaluation_time is not set + eligible_shipcalls = [shipcall for shipcall in eligible_shipcalls if shipcall.evaluation_time is not None] + + # filter out the shipcalls, where the evaluation_time is too recent. We expect a minimum difference, so user input errors do not cause notifications + eligible_shipcalls = [shipcall for shipcall in eligible_shipcalls if Notifier.check_shipcall_evaluation_time_exceeds_minimum_time_difference(shipcall, time_diff_threshold)] + return eligible_shipcalls + @staticmethod + def get_eligible_notifications_of_shipcall(shipcall:model.Shipcall, time_diff_threshold:float)->list[model.Notification]: + """obtain all notifications, which belong to the shipcall id""" + query = SQLQuery.get_notifications() + eligible_notifications = execute_sql_query_standalone(query=query, model=model.Notification, param={"scid" : shipcall.id}) + eligible_notifications = [notification for notification in eligible_notifications if Notifier.check_notification_level_matches_shipcall_entry(notification, shipcall)] + return eligible_notifications + + @staticmethod + def check_shipcall_evaluation_time_exceeds_minimum_time_difference(shipcall:model.Shipcall, time_diff_threshold:float): + """ + a notification may only be sent, when the created notification has been created or modified {time_diff_threshold} seconds ago. + """ + assert (shipcall.evaluation_time is not None), f"must provide 'evaluation_time'" + return difference_to_then(shipcall.evaluation_time)>=time_diff_threshold + + @staticmethod + def check_notification_level_matches_shipcall_entry(notification, shipcall): + """ + a notification may only be sent, when the shipcall entry matches the notification level. + otherwise, a user may have adapted the shipcall in the mean-time, so a notification would no longer be useful. + """ + return int(shipcall.evaluation) == int(notification.level) + + @staticmethod + def get_eligible_notifications(shipcalls:list[model.Shipcall]): + """obtain a list of all notifications of each element of the shipcall list.""" + eligible_notifications = [] + for shipcall in shipcalls: + eligible_notification = Notifier.get_eligible_notifications_of_shipcall(shipcall) + eligible_notifications.extend(eligible_notification) + raise NotImplementedError("refactoring!") + return eligible_notifications + + @staticmethod + def create_notifications_for_user_list(shipcall, users:list[model.User]): + raise NotImplementedError("deprecated") + notification_type_list = [] + for user in users: + user_notification_type_list = Notifier.build_notification_type_list(user) + notification_type_list.extend(user_notification_type_list) + + # get the unique notification types + notification_type_list = list(set(notification_type_list)) + for notification_type in notification_type_list: + schemaModel = dict(shipcall_id = shipcall.id, level = int(shipcall.evaluation), type = notification_type, message = "", created = datetime.datetime.now(), modified=None) + query = SQLQuery.get_notifications_post(schemaModel) + schemas = execute_sql_query_standalone(query=query, param=schemaModel, command_type="execute") + return + + @staticmethod + def generate_notification(shipcall:model.Shipcall, notification_type:model.NotificationType): + """ + This one-line method creates a notification for the provided shipcall and notification type + """ + schemaModel = dict( + shipcall_id = shipcall.id, + level = int(shipcall.evaluation), + type = notification_type, + message = "", # #TODO_messsage: what should be stored here? The HTML-template appears to be too long. + created = datetime.datetime.now(), + modified=None) + query = SQLQuery.get_notifications_post(schemaModel) + schemas = execute_sql_query_standalone(query=query, param=schemaModel, command_type="execute") + return + + @staticmethod + def DEPRECATED_generate_notifications(shipcall_id): + """ + # #TODO_delete: this method can be safely removed after 15.08.2024 + + This one-line method creates all notifications for the provided shipcall id. It does so by obtaining the shipcall, + looking up its history, and finding all attached users. + For each user, a notification will be created for each subscribed notification type (e.g., Email) + """ + shipcall = Notifier.get_shipcall(shipcall_id) + notifications = execute_sql_query_standalone(query=SQLQuery.get_notifications(), param={"scid" : shipcall_id}, model=model.Notification, command_type="query") + latest_notification = Notifier.find_latest_notification(notifications) + + old_state = model.EvaluationType(latest_notification.level) if latest_notification is not None else model.EvaluationType.undefined + new_state = shipcall.evaluation + + # identify, whether the severity of the shipcall has increased to see, whether a notification is required + severity_increase = Notifier.check_higher_severity(old_state=old_state, new_state=new_state) + + # when the severity increases, set the 'evaluation_notifications_sent' argument to 0 (False) + if severity_increase: + ### UPDATE Shipcall ### + # prepare and create a query + evaluation_notifications_sent = 0 + schemaModel = {"id":shipcall.id, "evaluation_notifications_sent":evaluation_notifications_sent} # #TODO: should this require the 'modified' tag to be adapted? + query = SQLQuery.get_shipcall_put(schemaModel) + + # execute the PUT-Request + schemas = execute_sql_query_standalone(query=query, param=schemaModel, command_type="execute") + + ### Generate Notifications ### + # find all attached users of the shipcall (checks the history, then reads out the user ids and builds the users) + users = Notifier.get_users_via_history(shipcall_id=shipcall.id) + + # for each user, identify the notification_types, which must be generated. Finally, create those + # notifications with a POST-request + Notifier.create_notifications_for_user_list(shipcall, users) + return + + @staticmethod + def create_etaetd_string(eta, etd): # #TODO_rename: function name is improvable + eta = eta.strftime("%d.%m.%Y %H:%M") if eta is not None else None + etd = etd.strftime("%d.%m.%Y %H:%M") if etd is not None else None + + eta_etd = "" + + if eta is not None and etd is not None: + eta_etd = f"{eta} - {etd}" + + if eta is not None and etd is None: + eta_etd = f"{eta}" + + if etd is None and etd is not None: + eta_etd = f"{etd}" + return eta_etd + + @staticmethod + def prepare_notification_body(shipcall:model.Shipcall): + # obtain the respective shipcall and ship + shipcall = execute_sql_query_standalone(query=SQLQuery.get_shipcall_by_id(), model=model.Shipcall, param={"id" : shipcall.id}, command_type="single") + ship = execute_sql_query_standalone(query=SQLQuery.get_ship_by_id(), model=model.Ship, param={"id" : shipcall.ship_id}, command_type="single") + + # use ship & shipcall data models to prepare the body + ship_name = ship.name + eta_etd = Notifier.create_etaetd_string(shipcall.eta, shipcall.etd) + eta_etd_type = eta_etd_type_dict[model.ShipcallType(shipcall.type)] + evaluation_message = shipcall.evaluation_message + return (ship_name, evaluation_message, eta_etd, eta_etd_type) + + @staticmethod + def shipcall_put_update_evaluation_notifications_sent_flag(shipcall): + # change the 'evaluation_notifications_sent' flag to 1 + evaluation_notifications_sent = 1 + schemaModel = {"id":shipcall.id, "evaluation_notifications_sent":evaluation_notifications_sent} + query = SQLQuery.get_shipcall_put(schemaModel) + + schemas = execute_sql_query_standalone(query=query, param=schemaModel, command_type="execute") + return + + @staticmethod + def build_email_targets_validation_notification(users)->list[str]: + # readout the email address of all users + email_tgts = [user.user_email for user in users if user.user_email is not None] + + # additionally, always inform the BSMD + email_tgts.append("bremencalling@bsmd.de") # #TODO: for testing, use "bremencalling@bsmd.de". For live system, use "report@bsmd.de" + + # #TODO_development: overwrite the recipients. Only send to 'bremencalling@bsmd.de' until the testing phase has succeeded. + email_tgts = ["bremencalling@bsmd.de" for tgt in email_tgts] + + # avoid multi-mails, when (for some reason) multiple users share the same email address. + email_tgts = list(set(email_tgts)) + return email_tgts + + @staticmethod + def create_and_send_notification_mapper(notification_type:model.NotificationType, shipcall:model.Shipcall, eligible_users:list[model.User], email_handler:EmailHandler, mail_pwd:bytes, debug:bool=False): + # #TODO_refactor: instead create a method, which contains the 'distribution-logic' for all notification types + if int(notification_type)==int(model.NotificationType.email): + # create an Email and send it to each eligible_user. + # #TODO: this method must be a distributor. It should send emails for those, who want emails, and provide placeholders for other types of notifications + Notifier.create_and_send_email_notification(email_handler, mail_pwd, eligible_users, shipcall, debug=debug) + + elif int(notification_type)==int(model.NotificationType.undefined): pass + elif int(notification_type)==int(model.NotificationType.push): pass + else: pass + return + + @staticmethod + def create_and_send_email_notification(email_handler:EmailHandler, pwd:bytes, users:list[model.User], shipcall:model.Shipcall, debug:bool=False): + """ + # #TODO_rename: when there is more than one type of notification, this should be renamed. This method refers to a validation-state notification + + this 'naive' method creates a message and simply sends it to all users in a list of users. + Afterwards, the database will be updated, so the shipcall no longer requires a notification. + """ + # get a list of all recipients + email_tgts = Notifier.build_email_targets_validation_notification(users) + + # prepare and build the Email content + content = get_default_html_email() + files = [] # optional attachments + ship_name, evaluation_message, eta_etd, eta_etd_type = Notifier.prepare_notification_body(shipcall) + + msg_multipart,msg_content = create_shipcall_evaluation_notification( + email_handler, ship_name, evaluation_message, eta_etd, eta_etd_type, content, files=files + ) + + # send the messages via smtlib's SSL functions + send_notification(email_handler, email_tgts, msg_multipart, pwd, debug=debug) + return + + @staticmethod + def check_user_is_subscribed_to_notification_type(user,notification_type): + """given a notification, one can check, whether the current user has subscribed to the respective notification_type. Returns a boolean""" + if int(notification_type) == int(model.NotificationType.email): + return user.notify_email + + elif int(notification_type) == int(model.NotificationType.push): + return user.notify_popup + + elif int(notification_type) == int(model.NotificationType.undefined): + pass + + ### placeholders: + #elif int(notification_type) == int(model.NotificationType.whatsapp): + #return user.notify_whatsapp + + #elif int(notification_type) == int(model.NotificationType.signal): + #return user.notify_signal + + else: # placeholder: whatsapp/signal + raise NotImplementedError(notification_type) + + @staticmethod + def get_all_notification_types(): + from BreCal.schemas import model + return list(model.NotificationType._member_map_.values()) -"""# build the list of evaluation times ('now', as isoformat)""" -#evaluation_times = [datetime.datetime.now().isoformat() for _i in range(len(evaluation_states_new))] \ No newline at end of file diff --git a/src/server/BreCal/resources/__init__.py b/src/server/BreCal/resources/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/server/BreCal/resources/logo_bremen_calling.png b/src/server/BreCal/resources/logo_bremen_calling.png new file mode 100644 index 0000000..bb97bf5 Binary files /dev/null and b/src/server/BreCal/resources/logo_bremen_calling.png differ diff --git a/src/server/BreCal/resources/warning_notification_email_template.txt b/src/server/BreCal/resources/warning_notification_email_template.txt new file mode 100644 index 0000000..819c668 --- /dev/null +++ b/src/server/BreCal/resources/warning_notification_email_template.txt @@ -0,0 +1,159 @@ + + +
+ + +| + |
+
+
+
+
+
+
|
+ + |