def patch(self, campaign_id): """ This endpoint updates an existing campaign :param int|long campaign_id: Id of campaign """ raise_if_dict_values_are_not_int_or_long(dict(campaign_id=campaign_id)) # Get and validate request data data = request.get_json(silent=True) if not data: raise InvalidUsage("Received empty request body", error_code=INVALID_REQUEST_BODY[1]) is_hidden = data.get('is_hidden', False) if is_hidden not in (True, False, 1, 0): raise InvalidUsage( "is_hidden field should be a boolean, given: %s" % is_hidden, error_code=INVALID_INPUT[1]) email_campaign = EmailCampaignBase.get_campaign_if_domain_is_valid( campaign_id, request.user) # Unschedule task from scheduler_service if email_campaign.scheduler_task_id: headers = {'Authorization': request.oauth_token} # campaign was scheduled, remove task from scheduler_service if CampaignUtils.delete_scheduled_task( email_campaign.scheduler_task_id, headers): email_campaign.update( scheduler_task_id='') # Delete scheduler task id email_campaign.update(is_hidden=is_hidden) logger.info("Email campaign(id:%s) has been archived successfully" % campaign_id) return dict(message="Email campaign (id: %s) updated successfully" % campaign_id), codes.OK
def get(self): """ GET /v1/email-campaigns Fetches all EmailCampaign objects from auth user's domain """ user = request.user page, per_page = get_pagination_params( request, error_code=INVALID_VALUE_OF_PAGINATION_PARAM[1]) sort_type = request.args.get('sort_type', 'DESC') search_keyword = request.args.get('search', '') sort_by = request.args.get('sort_by', 'added_datetime') is_hidden = request.args.get('is_hidden', 0) user_id = request.args.get('user_id') # Validation of query parameters if isinstance(user_id, basestring): if not user_id.strip().isdigit() or int(user_id) <= 0: raise InvalidUsage(NOT_NON_ZERO_NUMBER[0].format('`user_id`'), error_code=NOT_NON_ZERO_NUMBER[1]) if request.user.role.name != Role.TALENT_ADMIN \ and User.get_domain_id(user_id) != request.user.domain_id: raise ForbiddenError( "Logged-in user and requested user_id are of different domains", EMAIL_CAMPAIGN_FORBIDDEN[1]) user_id = int(user_id) if not is_number(is_hidden) or int(is_hidden) not in (0, 1): raise InvalidUsage('`is_hidden` can be either 0 or 1', error_code=INVALID_VALUE_OF_QUERY_PARAM[1]) if sort_by not in ('added_datetime', 'name'): raise InvalidUsage('Value of sort_by parameter is not valid', error_code=INVALID_VALUE_OF_QUERY_PARAM[1]) if sort_type not in SORT_TYPES: raise InvalidUsage( 'Value of sort_type parameter is not valid. Valid values are %s' % list(SORT_TYPES), error_code=INVALID_VALUE_OF_QUERY_PARAM[1]) # Get all email campaigns from logged in user's domain query = EmailCampaign.get_by_domain_id_and_filter_by_name( user.domain_id, search_keyword, sort_by, sort_type, int(is_hidden), user_id=user_id) return get_paginated_response('email_campaigns', query, page, per_page, parser=EmailCampaign.to_dict)
def validate_and_format_data_for_email_template_creation(data, domain_id): """ Validates the data for creation of email-template-folder :param dict data: Data received from UI :param int|long domain_id: Id of domain of logged-in user :return: Dictionary of formatted data :rtype: dict """ # Validation of required fields validate_required_fields(data, ('name', 'body_html'), error_code=MISSING_FIELD[1]) template_name = data['name'] body_html = data['body_html'] template_folder_id = None # Raise errors if invalid input of string inputs if [item for item in (template_name, body_html) if not (isinstance(item, basestring) and str(item).strip())]: raise InvalidUsage("Expecting `name` and `body_html` as non-empty string", error_code=INVALID_INPUT[1]) # Check if the name is already exists in the domain existing_template = UserEmailTemplate.get_by_name_and_domain_id(template_name, domain_id) if existing_template: raise InvalidUsage('Email template with name=%s already exists in the domain.' % template_name, error_code=DUPLICATE_TEMPLATE_NAME[1]) if 'template_folder_id' in data: template_folder_id = data['template_folder_id'] if template_folder_id is None or not isinstance(template_folder_id, (int, long)) \ or (isinstance(template_folder_id, (int, long)) and template_folder_id <= 0): raise InvalidUsage('Expecting template_folder_id to be positive integer', INVALID_INPUT[1]) # Validate parent_id is valid EmailTemplateFolder.get_valid_template_folder(template_folder_id, domain_id) # If is_immutable value is not passed, make it as 0 is_immutable = data.get('is_immutable', 0) if is_immutable is None or is_immutable not in (0, 1): raise InvalidUsage(error_message='Invalid input: is_immutable should be integer with value 0 or 1', error_code=INVALID_INPUT[1]) # strip whitespaces and return data return { 'name': template_name.strip(), 'body_html': body_html.strip(), 'body_text': data.get('body_text'), 'template_folder_id': template_folder_id, 'is_immutable': is_immutable }
def validate_and_format_data_for_template_folder_creation(data, domain_id): """ Validates the data for creation of email-template-folder :param dict data: Data received from UI :param int|long domain_id: Id of domain of logged-in user :return: Dictionary of formatted data :rtype: dict """ parent_id = None folder_name = data.get('name') # Validation of required fields validate_required_fields(data, ('name',), error_code=MISSING_FIELD[1]) # Validation of folder name if not isinstance(folder_name, basestring) or not str(folder_name).strip(): raise InvalidUsage('Invalid input: Folder name must be a valid string.', error_code=INVALID_INPUT[1]) # Check if the name already exists under same domain duplicate = EmailTemplateFolder.get_by_name_and_domain_id(folder_name, domain_id) if duplicate: raise InvalidUsage(DUPLICATE_TEMPLATE_FOLDER_NAME[0], error_code=DUPLICATE_TEMPLATE_FOLDER_NAME[1]) if 'parent_id' in data: parent_id = data['parent_id'] # Validate parent_id is valid if parent_id is None or not isinstance(parent_id, (int, long)) \ or (isinstance(parent_id, (int, long)) and parent_id <= 0): raise InvalidUsage('Expecting parent_id to be positive integer', INVALID_INPUT[1]) EmailTemplateFolder.get_valid_template_folder(parent_id, request.user.domain_id) # If is_immutable value is not passed, make it as 0 is_immutable = data.get('is_immutable', 0) if is_immutable is None or is_immutable not in (0, 1): raise InvalidUsage(error_message='Invalid input: is_immutable should be integer with value 0 or 1', error_code=INVALID_INPUT[1]) # strip whitespaces and return data return { 'name': folder_name.strip(), 'parent_id': parent_id, 'is_immutable': is_immutable }
def post(self): """ This will save an entry in database table email_client_credentials. .. Request Body:: { "host": "Host Name", "port": 123, "name": "Server Name", "email": "*****@*****.**", "password": "******", } .. Response:: { "id": 347 } .. Status:: 201 (Resource created) 400 (Bad request) 401 (Unauthorized to access getTalent) 500 (Internal server error) """ data = get_json_data_if_validated(request, EMAIL_CLIENTS_SCHEMA) data = format_email_client_data(data) data['user_id'] = request.user.id client_in_db = EmailClientCredentials.get_by_user_id_host_and_email( data['user_id'], data['host'], data['email']) if client_in_db: raise InvalidUsage( 'Email client with given data already present in database') client = EmailClientBase.get_client(data['host']) client = client(data['host'], data['port'], data['email'], data['password']) logger.info('Connecting with given email-client') client.connect() client.authenticate() logger.info( 'Successfully connected and authenticated with given email-client') # Encrypt password ciphered_password = encrypt( app.config[TalentConfigKeys.ENCRYPTION_KEY], data['password']) b64_password = b64encode(ciphered_password) data['password'] = b64_password email_client = EmailClientCredentials(**data) EmailClientCredentials.save(email_client) headers = { 'Location': EmailCampaignApiUrl.EMAIL_CLIENT_WITH_ID % email_client.id } return ApiResponse(dict(id=email_client.id), status=requests.codes.CREATED, headers=headers)
def authenticate(self, connection_quit=True): """ This first connects with POP server. It then tries to login to server. """ try: self.connection.user(self.email) self.connection.pass_(self.password) except poplib.error_proto as error: logger.exception(error.message) raise InvalidUsage( 'Invalid credentials provided. Could not authenticate with POP server', additional_error_info=dict(pop_error=error.message))
def authenticate(self, connection_quit=True): """ This first connects with IMAP server. It then tries to login to server. :type connection_quit: bool """ try: self.connection.login(self.email, self.password) except imaplib.IMAP4_SSL.error as error: logger.exception(error.message) raise InvalidUsage( 'Could not authenticate with IMAP server.', additional_error_info=dict(imap_error=error.message))
def post(self): """ This creates a base campaign with following payload { "name": "Jobs at getTalent "Description": "We are looking for Python developers } """ user = request.user data = request.get_json(silent=True) if not data: raise InvalidUsage("Received empty request body") name = data.get('name', '') description = data.get('description', '') if not name or not description: raise InvalidUsage('Name and description are required fields') base_campaign = BaseCampaign(user_id=user.id, name=name, description=description) base_campaign.save() return {'id': base_campaign.id}, codes.CREATED
def do_prefs_url_replacement(text, candidate, candidate_address): """ Here we do the replacement of merge tag "*|PREFERENCES_URL|*". After replacement this will become the URL for the candidate to unsubscribe the email-campaign. :param string text: This maybe subject, body_html or body_text of email-campaign :param Candidate candidate: Object of candidate to which email-campaign is supposed to be sent :param basestring candidate_address: Address of Candidate to which email campaign is being sent :rtype: string """ candidate_id = candidate.id if not (isinstance(text, basestring) and text): raise InvalidUsage('Text should be non-empty string') if not (isinstance(candidate_id, (int, long)) and candidate_id): raise InvalidUsage('candidate_id should be positive int"long') host_name = get_web_app_url() secret_key_id = jwt_security_key() secret_key = redis_store.get(secret_key_id) s = Serializer(secret_key, expires_in=SIX_MONTHS_EXPIRATION_TIME) payload = {"candidate_id": candidate_id} unsubscribe_url = host_name + ( '/candidates/%s/preferences?%s' % (str(candidate_id), urllib.urlencode({ 'token': '%s.%s' % (s.dumps(payload), secret_key_id), 'email': candidate_address or '' }))) # In case the user accidentally wrote http://*|PREFERENCES_URL|* or https://*|PREFERENCES_URL|* text = text.replace("http://" + DEFAULT_PREFERENCES_URL_MERGETAG, unsubscribe_url) text = text.replace("https://" + DEFAULT_PREFERENCES_URL_MERGETAG, unsubscribe_url) # The normal case text = text.replace(DEFAULT_PREFERENCES_URL_MERGETAG, unsubscribe_url) return text
def get_client(host): """ This gets the required client for given host. :param string host: Hostname e.g. smtp.gmail.com """ host = host.strip().lower() if 'smtp' in host: client = SMTP elif 'imap' in host: client = IMAP elif 'pop' in host: client = POP else: raise InvalidUsage('Unknown host provided') return client
def test_create_email_campaign_with_invalid_email_client_id( self, access_token_first, smartlist_user1_domain1_in_db): """ Here we try to create an email-campaign with invalid email-client-id. It should result in invalid usage error. """ campaign_data = create_data_for_campaign_creation( smartlist_user1_domain1_in_db['id']) campaign_data[ 'email_client_id'] = CampaignsTestsHelpers.get_non_existing_id( EmailClient) response = create_email_campaign_via_api(access_token_first, campaign_data) assert response.status_code == InvalidUsage.http_status_code() json_response = response.json() assert 'email_client_id' in json_response['error']['message']
def validate_data_to_schedule_campaign(campaign_data): """ This validates the data provided to schedule a campaign. - Get number of seconds by validating given frequency_id - If end_datetime is not given and frequency is for periodic task, we raise Invalid usage. - Returns frequency_id, start_datetime and end_datetime This function is used in data_validation_for_campaign_schedule() of CampaignBase class. :param dict campaign_data: Campaign data :rtype: tuple """ start_datetime_obj, end_datetime_obj = None, None frequency_id = campaign_data.get('frequency_id') # required # Get number of seconds from frequency_id. If frequency is there then there must be a start time. try: frequency = Frequency.get_seconds_from_id(frequency_id) except InvalidUsage as error: raise InvalidUsage(error.message, error_code=INVALID_INPUT[1]) # Get start datetime string start_datetime = campaign_data.get('start_datetime') # Get end datetime string end_datetime = campaign_data.get('end_datetime') if frequency and not start_datetime: raise UnprocessableEntity("Frequency requires `start_datetime`.", error_code=MISSING_FIELD[1]) if frequency and not end_datetime: raise UnprocessableEntity("`end_datetime` is required to schedule a periodic task", error_code=MISSING_FIELD[1]) # Validate format and value if start_datetime: start_datetime_obj = DatetimeUtils.get_datetime_obj_if_format_is_valid(start_datetime, error_code=INVALID_DATETIME_FORMAT[1]) if not DatetimeUtils(start_datetime_obj).is_in_future(): raise UnprocessableEntity('`start_datetime` must be in future. Given {}'.format(start_datetime), error_code=INVALID_DATETIME_VALUE[1]) if end_datetime: end_datetime_obj = DatetimeUtils.get_datetime_obj_if_format_is_valid(end_datetime, error_code=INVALID_DATETIME_FORMAT[1]) if not DatetimeUtils(end_datetime_obj).is_in_future(): raise UnprocessableEntity('`end_datetime` must be in future. Given {}'.format(end_datetime), error_code=INVALID_DATETIME_VALUE[1]) if start_datetime and end_datetime and start_datetime_obj > end_datetime_obj: raise UnprocessableEntity("`end_datetime` cannot be before `start_datetime`", error_code=INVALID_DATETIME_VALUE[1]) return frequency_id, frequency, start_datetime, end_datetime
def authenticate(self, connection_quit=True): """ This first connects with SMTP server. It then tries to login to server. """ try: self.connection.login(self.email, self.password) except smtplib.SMTPAuthenticationError as error: logger.exception(error.smtp_error) raise InvalidUsage( 'Invalid credentials provided. Could not authenticate with SMTP server', additional_error_info=dict(smtp_error=error.smtp_error)) except smtplib.SMTPException as error: logger.exception(error.message) raise InternalServerError( 'Could not authenticate with SMTP server', additional_error_info=dict(smtp_error=error.message)) if connection_quit: self.connection.quit()
def validate_and_format_request_data(data, current_user): """ Validates the request form data and returns the formatted data with leading and trailing white spaces stripped. :param dict data: Data received from UI :param User current_user: Logged-in user's object :return: Dictionary of formatted data :rtype: dict """ name = data.get('name') # required subject = data.get('subject') # required description = data.get('description', '') # required _from = data.get('from') reply_to = data.get('reply_to') body_html = data.get('body_html') # required body_text = data.get('body_text') list_ids = data.get('list_ids') # required email_client_id = data.get('email_client_id') frequency_id = data.get('frequency_id') # required email_client_credentials_id = data.get('email_client_credentials_id') base_campaign_id = data.get('base_campaign_id') # Find if any required key has no valid value validate_required_fields(data, EmailCampaignBase.REQUIRED_FIELDS, error_code=MISSING_FIELD[1]) # Raise errors if invalid input of string inputs if [item for item in (name, subject, body_html) if not (isinstance(item, basestring) and str(item).strip())]: raise InvalidUsage("Expecting `name`, `subject` and `body_html` as non-empty string", error_code=INVALID_INPUT[1]) if not frequency_id: raise InvalidUsage(INVALID_INPUT[0].format("`frequency_id`"), error_code=INVALID_INPUT[1]) # Validation for list ids belonging to same domain validate_smartlist_ids(list_ids, current_user, error_code=INVALID_INPUT[1], resource_not_found_error_code=SMARTLIST_NOT_FOUND[1], forbidden_error_code=SMARTLIST_FORBIDDEN[1]) frequency_id, frequency, start_datetime, end_datetime = validate_data_to_schedule_campaign(data) if email_client_id: # Check if email_client_id is valid email_client = EmailClient.query.get(email_client_id) if not email_client: raise InvalidUsage("`email_client_id` is not valid id.") # TODO: Add custom error code in GET-2573 # In case user wants to send email-campaign with its own account if email_client_credentials_id: email_client_credentials = EmailClientCredentials.get_by_id(email_client_credentials_id) if not EmailClientBase.is_outgoing(email_client_credentials.host): raise InvalidUsage("Selected email-client must be of type `outgoing`") # TODO: Add custom error code in GET-2556 # Validation for base_campaign_id if base_campaign_id: validate_base_campaign_id(base_campaign_id, current_user.domain_id) # strip whitespaces and return data return { 'name': name.strip(), 'subject': subject.strip(), 'description': description.strip(), 'from': get_or_set_valid_value(_from, basestring, '').strip(), 'reply_to': get_or_set_valid_value(reply_to, basestring, '').strip(), 'body_html': body_html.strip(), 'body_text': get_or_set_valid_value(body_text, basestring, '').strip(), 'list_ids': list_ids, 'email_client_id': email_client_id, 'start_datetime': start_datetime, 'end_datetime': end_datetime, 'frequency_id': frequency_id, 'email_client_credentials_id': email_client_credentials_id, 'base_campaign_id': base_campaign_id }
def get(self, base_campaign_id): """ This resource returns event and all chained campaigns with given base_campaign_id. ..Response:: { "event": { "cost": 0, "start_datetime": "2016-08-13 16:21:42", "venue_id": 307, "user_id": 1, "is_deleted_from_vendor": 0, "description": "Test Event Description", "social_network_id": 13, "url": "", "title": "Eventbrite Test Event", "registration_instruction": "Just Come", "max_attendees": 10, "timezone": "Asia/Karachi", "currency": "USD", "venue": { "city": "Lahore", "user_id": 1, "social_network_id": 18, "country": "", "longitude": 0, "social_network_venue_id": "16271034", "state": "Punjab", "latitude": 0, "zip_code": "", "address_line_2": "H# 163, Block A", "id": 307, "address_line_1": "New Muslim Town" }, "rsvps": [ { "social_network_rsvp_id": "6956", "status": "yes", "social_network_id": 13, "event_id": 1, "payment_status": "", "datetime": "", "candidate_id": 362553, "id": 2 }, { "social_network_rsvp_id": "2983", "status": "yes", "social_network_id": 13, "event_id": 1, "payment_status": "", "datetime": "", "candidate_id": 362555, "id": 3 }, { "social_network_rsvp_id": "5146", "status": "yes", "social_network_id": 13, "event_id": 1, "payment_status": "", "datetime": "", "candidate_id": 362557, "id": 4 } ], "tickets_id": 53364240, "social_network_group_id": "18837246", "group_url_name": "QC-Python-Learning", "organizer_id": 33, "base_campaign_id": 1, "id": 1, "social_network_event_id": "27067814562", "end_datetime": "2016-08-14 16:21:42" }, "email_campaigns": [ { "email_client_credentials_id": null, "start_datetime": null, "user_id": 1, "name": "Email campaign", "body_text": null, "description": null, "list_ids": [1], "body_html": null, "blasts": [ { "updated_datetime": "2016-02-10 20:35:57", "sends": 0, "bounces": 0, "campaign_id": 11, "text_clicks": 0, "html_clicks": 0, "complaints": 0, "id": 15, "opens": 0, "sent_datetime": "2016-02-10 20:38:39" }, { "updated_datetime": "2016-02-10 20:35:57", "sends": 0, "bounces": 0, "campaign_id": 11, "text_clicks": 0, "html_clicks": 0, "complaints": 0, "id": 16, "opens": 0, "sent_datetime": "2016-02-10 20:38:40" } ], "added_datetime": "2016-02-10T20:35:57+00:00", "frequency": null, "end_datetime": null, "talent_pipelines": [], "reply_to": "*****@*****.**", "from": "basit", "is_hidden": false, "base_campaign_id": 1, "id": 11, "subject": "email campaign sample subject" }, { "email_client_credentials_id": null, "start_datetime": null, "user_id": 1, "name": "Email campaign", "body_text": null, "description": null, "list_ids": [1], "body_html": null, "blasts": [ { "updated_datetime": "2016-02-10 20:39:54", "sends": 1, "bounces": 0, "campaign_id": 12, "text_clicks": 0, "html_clicks": 0, "complaints": 0, "id": 18, "opens": 0, "sent_datetime": "2016-02-10 20:39:44" } ], "added_datetime": "2016-02-10T20:35:57+00:00", "frequency": null, "end_datetime": null, "talent_pipelines": [], "reply_to": "*****@*****.**", "from": "basit", "is_hidden": false, "base_campaign_id": 1, "id": 12, "subject": "email campaign sample subject" } ] } """ json_event = None email_campaigns_list = [] validate_base_campaign_id(base_campaign_id, request.user.domain_id) base_campaign = BaseCampaign.get_by_id(base_campaign_id) base_campaign_event = BaseCampaignEvent.filter_by_keywords(base_campaign_id=base_campaign_id) if base_campaign_event: event = base_campaign_event[0].event # Pick first associated event json_event = event.to_json() json_event['rsvps'] = [rsvp.to_json() for rsvp in event.rsvps.all()] json_event['venue'] = event.venue.to_json() if event.venue else {} email_campaigns = base_campaign.email_campaigns.all() if not json_event and not email_campaigns: raise InvalidUsage('Requested base campaign is orphaned') for email_campaign in email_campaigns: json_email_campaign = email_campaign.to_dict() json_email_campaign['blasts'] = [blast.to_json() for blast in email_campaign.blasts.all()] email_campaigns_list.append(json_email_campaign) return {'event': json_event, 'email_campaigns': email_campaigns_list}