diff --git a/backend/src/app/config.clj b/backend/src/app/config.clj
index d5f1ce587..51741e329 100644
--- a/backend/src/app/config.clj
+++ b/backend/src/app/config.clj
@@ -11,10 +11,10 @@
   "A configuration management."
   (:refer-clojure :exclude [get])
   (:require
-   [clojure.core :as c]
    [app.common.spec :as us]
    [app.common.version :as v]
    [app.util.time :as dt]
+   [clojure.core :as c]
    [clojure.spec.alpha :as s]
    [cuerdas.core :as str]
    [environ.core :refer [env]]))
@@ -54,6 +54,12 @@
    :smtp-default-reply-to "Penpot <no-reply@example.com>"
    :smtp-default-from "Penpot <no-reply@example.com>"
 
+   :profile-complaint-max-age (dt/duration {:days 7})
+   :profile-complaint-threshold 2
+
+   :profile-bounce-max-age (dt/duration {:days 7})
+   :profile-bounce-threshold 10
+
    :allow-demo-users true
    :registration-enabled true
    :registration-domain-whitelist ""
@@ -100,6 +106,11 @@
 (s/def ::feedback-enabled ::us/boolean)
 (s/def ::feedback-destination ::us/string)
 
+(s/def ::profile-complaint-max-age ::dt/duration)
+(s/def ::profile-complaint-threshold ::us/integer)
+(s/def ::profile-bounce-max-age ::dt/duration)
+(s/def ::profile-bounce-threshold ::us/integer)
+
 (s/def ::error-report-webhook ::us/string)
 
 (s/def ::smtp-enabled ::us/boolean)
@@ -187,6 +198,10 @@
                    ::ldap-bind-dn
                    ::ldap-bind-password
                    ::public-uri
+                   ::profile-complaint-threshold
+                   ::profile-bounce-threshold
+                   ::profile-complaint-max-age
+                   ::profile-bounce-max-age
                    ::redis-uri
                    ::registration-domain-whitelist
                    ::registration-enabled
diff --git a/backend/src/app/emails.clj b/backend/src/app/emails.clj
index 68441d821..74fbaf84b 100644
--- a/backend/src/app/emails.clj
+++ b/backend/src/app/emails.clj
@@ -5,13 +5,15 @@
 ;; This Source Code Form is "Incompatible With Secondary Licenses", as
 ;; defined by the Mozilla Public License, v. 2.0.
 ;;
-;; Copyright (c) 2020 UXBOX Labs SL
+;; Copyright (c) 2020-2021 UXBOX Labs SL
 
 (ns app.emails
   "Main api for send emails."
   (:require
    [app.common.spec :as us]
    [app.config :as cfg]
+   [app.db :as db]
+   [app.db.sql :as sql]
    [app.tasks :as tasks]
    [app.util.emails :as emails]
    [clojure.spec.alpha :as s]))
@@ -41,6 +43,54 @@
                          :priority 200
                          :props email})))
 
+
+(def sql:profile-complaint-report
+  "select (select count(*)
+             from profile_complaint_report
+            where type = 'complaint'
+              and profile_id = ?
+              and created_at > now() - ?::interval) as complaints,
+          (select count(*)
+             from profile_complaint_report
+            where type = 'bounce'
+              and profile_id = ?
+              and created_at > now() - ?::interval) as bounces;")
+
+(defn allow-send-emails?
+  [conn profile]
+  (when-not (:is-muted profile false)
+    (let [complaint-threshold (cfg/get :profile-complaint-threshold)
+          complaint-max-age   (cfg/get :profile-complaint-max-age)
+          bounce-threshold    (cfg/get :profile-bounce-threshold)
+          bounce-max-age      (cfg/get :profile-bounce-max-age)
+
+          {:keys [complaints bounces] :as result}
+          (db/exec-one! conn [sql:profile-complaint-report
+                              (:id profile)
+                              (db/interval complaint-max-age)
+                              (:id profile)
+                              (db/interval bounce-max-age)])]
+
+      (and (< complaints complaint-threshold)
+           (< bounces bounce-threshold)))))
+
+(defn has-complaint-reports?
+  ([conn email] (has-complaint-reports? conn email nil))
+  ([conn email {:keys [threshold] :or {threshold 1}}]
+   (let [reports (db/exec! conn (sql/select :global-complaint-report
+                                            {:email email :type "complaint"}
+                                            {:limit 10}))]
+     (>= (count reports) threshold))))
+
+(defn has-bounce-reports?
+  ([conn email] (has-bounce-reports? conn email nil))
+  ([conn email {:keys [threshold] :or {threshold 1}}]
+   (let [reports (db/exec! conn (sql/select :global-complaint-report
+                                            {:email email :type "bounce"}
+                                            {:limit 10}))]
+     (>= (count reports) threshold))))
+
+
 ;; --- Emails
 
 (s/def ::subject ::us/string)
diff --git a/backend/src/app/http.clj b/backend/src/app/http.clj
index ffe534971..1158f34b9 100644
--- a/backend/src/app/http.clj
+++ b/backend/src/app/http.clj
@@ -126,6 +126,9 @@
     ["/dbg"
      ["/error-by-id/:id" {:get (:error-report-handler cfg)}]]
 
+    ["/webhooks"
+     ["/sns" {:post (:sns-webhook cfg)}]]
+
     ["/api" {:middleware [[middleware/format-response-body]
                           [middleware/params]
                           [middleware/multipart-params]
diff --git a/backend/src/app/http/auth/github.clj b/backend/src/app/http/auth/github.clj
index c01200126..26753bc05 100644
--- a/backend/src/app/http/auth/github.clj
+++ b/backend/src/app/http/auth/github.clj
@@ -110,6 +110,7 @@
 
           method-fn (get-in rpc [:methods :mutation :login-or-register])
           profile   (method-fn {:email (:email info)
+                                :backend "github"
                                 :fullname (:fullname info)})
           token     (tokens :generate
                             {:iss :auth
diff --git a/backend/src/app/http/auth/gitlab.clj b/backend/src/app/http/auth/gitlab.clj
index 5e78f1071..f253016b0 100644
--- a/backend/src/app/http/auth/gitlab.clj
+++ b/backend/src/app/http/auth/gitlab.clj
@@ -112,6 +112,7 @@
 
           method-fn (get-in rpc [:methods :mutation :login-or-register])
           profile   (method-fn {:email (:email info)
+                                :backend "gitlab"
                                 :fullname (:fullname info)})
           token     (tokens :generate {:iss :auth
                                        :exp (dt/in-future "15m")
diff --git a/backend/src/app/http/auth/google.clj b/backend/src/app/http/auth/google.clj
index 12a2d7034..653352d42 100644
--- a/backend/src/app/http/auth/google.clj
+++ b/backend/src/app/http/auth/google.clj
@@ -98,6 +98,7 @@
                             :code :unable-to-auth))
           method-fn (get-in rpc [:methods :mutation :login-or-register])
           profile   (method-fn {:email (:email info)
+                                :backend "google"
                                 :fullname (:fullname info)})
           token     (tokens :generate {:iss :auth
                                        :exp (dt/in-future "15m")
diff --git a/backend/src/app/http/auth/ldap.clj b/backend/src/app/http/auth/ldap.clj
index ec6dc16dc..41e260e9f 100644
--- a/backend/src/app/http/auth/ldap.clj
+++ b/backend/src/app/http/auth/ldap.clj
@@ -64,6 +64,7 @@
                                                 :password (:password data)))]
             (let [method-fn (get-in rpc [:methods :mutation :login-or-register])
                   profile   (method-fn {:email (:email info)
+                                        :backend "ldap"
                                         :fullname (:fullname info)})
 
                   sxf       ((:create session) (:id profile))
diff --git a/backend/src/app/http/awsns.clj b/backend/src/app/http/awsns.clj
new file mode 100644
index 000000000..9443b90eb
--- /dev/null
+++ b/backend/src/app/http/awsns.clj
@@ -0,0 +1,207 @@
+;; This Source Code Form is subject to the terms of the Mozilla Public
+;; License, v. 2.0. If a copy of the MPL was not distributed with this
+;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
+;;
+;; This Source Code Form is "Incompatible With Secondary Licenses", as
+;; defined by the Mozilla Public License, v. 2.0.
+;;
+;; Copyright (c) 2020 Andrey Antukh <niwi@niwi.nz>
+
+(ns app.http.awsns
+  "AWS SNS webhook handler for bounces."
+  (:require
+   [app.common.exceptions :as ex]
+   [app.db :as db]
+   [app.db.sql :as sql]
+   [app.util.http :as http]
+   [clojure.pprint :refer [pprint]]
+   [clojure.spec.alpha :as s]
+   [clojure.tools.logging :as log]
+   [cuerdas.core :as str]
+   [integrant.core :as ig]
+   [jsonista.core :as j]))
+
+(declare parse-json)
+(declare parse-notification)
+(declare process-report)
+
+(defn- pprint-report
+  [message]
+  (binding [clojure.pprint/*print-right-margin* 120]
+    (with-out-str (pprint message))))
+
+(defmethod ig/pre-init-spec ::handler [_]
+  (s/keys :req-un [::db/pool]))
+
+(defmethod ig/init-key ::handler
+  [_ cfg]
+  (fn [request]
+    (let [body  (parse-json (slurp (:body request)))
+          mtype (get body "Type")]
+      (cond
+        (= mtype "SubscriptionConfirmation")
+        (let [surl   (get body "SubscribeURL")
+              stopic (get body "TopicArn")]
+          (log/infof "Subscription received (topic=%s, url=%s)" stopic surl)
+          (http/send! {:uri surl :method :post :timeout 10000}))
+
+        (= mtype "Notification")
+        (when-let [message (parse-json (get body "Message"))]
+          ;; (log/infof "Received: %s" (pr-str message))
+          (let [notification (parse-notification cfg message)]
+            (process-report cfg notification)))
+
+        :else
+        (log/warn (str "Unexpected data received.\n"
+                       (pprint-report body))))
+
+      {:status 200 :body ""})))
+
+(defn- parse-bounce
+  [data]
+  {:type        "bounce"
+   :kind        (str/lower (get data "bounceType"))
+   :category    (str/lower (get data "bounceSubType"))
+   :feedback-id (get data "feedbackId")
+   :timestamp   (get data "timestamp")
+   :recipients  (->> (get data "bouncedRecipients")
+                     (mapv (fn [item]
+                             {:email  (str/lower (get item "emailAddress"))
+                              :status (get item "status")
+                              :action (get item "action")
+                              :dcode  (get item "diagnosticCode")})))})
+
+(defn- parse-complaint
+  [data]
+  {:type          "complaint"
+   :user-agent    (get data "userAgent")
+   :kind          (get data "complaintFeedbackType")
+   :category      (get data "complaintSubType")
+   :timestamp     (get data "arrivalDate")
+   :feedback-id   (get data "feedbackId")
+   :recipients    (->> (get data "complainedRecipients")
+                       (mapv #(get % "emailAddress"))
+                       (mapv str/lower))})
+
+(defn- extract-headers
+  [mail]
+  (reduce (fn [acc item]
+            (let [key (get item "name")
+                  val (get item "value")]
+              (assoc acc (str/lower key) val)))
+          {}
+          (get mail "headers")))
+
+(defn- extract-identity
+  [{:keys [tokens] :as cfg} headers]
+  (let [tdata (get headers "x-penpot-data")]
+    (when-not (str/empty? tdata)
+      (let [result (tokens :verify {:token tdata :iss :profile-identity})]
+        (:profile-id result)))))
+
+(defn- parse-notification
+  [cfg message]
+  (let [type (get message "notificationType")
+        data (case type
+               "Bounce" (parse-bounce (get message "bounce"))
+               "Complaint" (parse-complaint (get message "complaint"))
+               {:type (keyword (str/lower type))
+                :message message})]
+    (when data
+      (let [mail (get message "mail")]
+        (when-not mail
+          (ex/raise :type :internal
+                    :code :incomplete-notification
+                    :hint "no email data received, please enable full headers report"))
+        (let [headers (extract-headers mail)
+              mail    {:destination (get mail "destination")
+                       :source      (get mail "source")
+                       :timestamp   (get mail "timestamp")
+                       :subject     (get-in mail ["commonHeaders" "subject"])
+                       :headers     headers}]
+          (assoc data
+                 :mail mail
+                 :profile-id (extract-identity cfg headers)))))))
+
+(defn- parse-json
+  [v]
+  (ex/ignoring
+   (j/read-value v)))
+
+(defn- register-bounce-for-profile
+  [{:keys [pool]} {:keys [type kind profile-id] :as report}]
+  (when (= kind "permanent")
+    (db/with-atomic [conn pool]
+      (db/insert! conn :profile-complaint-report
+                  {:profile-id profile-id
+                   :type (name type)
+                   :content (db/tjson report)})
+
+      ;; TODO: maybe also try to find profiles by mail and if exists
+      ;; register profile reports for them?
+      (doseq [recipient (:recipients report)]
+        (db/insert! conn :global-complaint-report
+                    {:email (:email recipient)
+                     :type (name type)
+                     :content (db/tjson report)}))
+
+      (let [profile (db/exec-one! conn (sql/select :profile {:id profile-id}))]
+        (when (some #(= (:email profile) (:email %)) (:recipients report))
+          ;; If the report matches the profile email, this means that
+          ;; the report is for itself, can be caused when a user
+          ;; registers with an invalid email or the user email is
+          ;; permanently rejecting receiving the email. In this case we
+          ;; have no option to mark the user as muted (and in this case
+          ;; the profile will be also inactive.
+          (db/update! conn :profile
+                      {:is-muted true}
+                      {:id profile-id}))))))
+
+(defn- register-complaint-for-profile
+  [{:keys [pool]} {:keys [type profile-id] :as report}]
+  (db/with-atomic [conn pool]
+    (db/insert! conn :profile-complaint-report
+                {:profile-id profile-id
+                 :type (name type)
+                 :content (db/tjson report)})
+
+    ;; TODO: maybe also try to find profiles by email and if exists
+    ;; register profile reports for them?
+    (doseq [email (:recipients report)]
+      (db/insert! conn :global-complaint-report
+                  {:email email
+                   :type (name type)
+                   :content (db/tjson report)}))
+
+    (let [profile (db/exec-one! conn (sql/select :profile {:id profile-id}))]
+      (when (some #(= % (:email profile)) (:recipients report))
+        ;; If the report matches the profile email, this means that
+        ;; the report is for itself, rare case but can happen; In this
+        ;; case just mark profile as muted (very rare case).
+        (db/update! conn :profile
+                    {:is-muted true}
+                    {:id profile-id})))))
+
+(defn- process-report
+  [cfg {:keys [type profile-id] :as report}]
+  (log/debug (str "Procesing report:\n" (pprint-report report)))
+  (cond
+    ;; In this case we receive a bounce/complaint notification without
+    ;; confirmed identity, we just emit a warning but do nothing about
+    ;; it because this is not a normal case. All notifications should
+    ;; come with profile identity.
+    (nil? profile-id)
+    (log/warn (str "A notification without identity recevied from AWS\n"
+                   (pprint-report report)))
+
+    (= "bounce" type)
+    (register-bounce-for-profile cfg report)
+
+    (= "complaint" type)
+    (register-complaint-for-profile cfg report)
+
+    :else
+    (log/warn (str "Unrecognized report received from AWS\n"
+                   (pprint-report report)))))
+
+
diff --git a/backend/src/app/main.clj b/backend/src/app/main.clj
index e26d84a92..6f2203445 100644
--- a/backend/src/app/main.clj
+++ b/backend/src/app/main.clj
@@ -71,6 +71,10 @@
     {:pool        (ig/ref :app.db/pool)
      :cookie-name "auth-token"}
 
+    :app.http.awsns/handler
+    {:tokens  (ig/ref :app.tokens/tokens)
+     :pool    (ig/ref :app.db/pool)}
+
     :app.http/server
     {:port    (:http-server-port config)
      :handler (ig/ref :app.http/router)
@@ -90,6 +94,7 @@
      :assets      (ig/ref :app.http.assets/handlers)
      :svgparse    (ig/ref :app.svgparse/handler)
      :storage     (ig/ref :app.storage/storage)
+     :sns-webhook (ig/ref :app.http.awsns/handler)
      :error-report-handler (ig/ref :app.error-reporter/handler)}
 
     :app.http.assets/handlers
diff --git a/backend/src/app/migrations.clj b/backend/src/app/migrations.clj
index 2d247e11c..7b0a19e6c 100644
--- a/backend/src/app/migrations.clj
+++ b/backend/src/app/migrations.clj
@@ -148,6 +148,10 @@
 
    {:name "0045-add-index-to-file-change-table"
     :fn (mg/resource "app/migrations/sql/0045-add-index-to-file-change-table.sql")}
+
+   {:name "0046-add-profile-complaint-table"
+    :fn (mg/resource "app/migrations/sql/0046-add-profile-complaint-table.sql")}
+
    ])
 
 
diff --git a/backend/src/app/migrations/sql/0046-add-profile-complaint-table.sql b/backend/src/app/migrations/sql/0046-add-profile-complaint-table.sql
new file mode 100644
index 000000000..431f73744
--- /dev/null
+++ b/backend/src/app/migrations/sql/0046-add-profile-complaint-table.sql
@@ -0,0 +1,45 @@
+CREATE TABLE profile_complaint_report (
+  profile_id uuid NOT NULL REFERENCES profile(id) ON DELETE CASCADE,
+  created_at timestamptz NOT NULL DEFAULT now(),
+
+  type text NOT NULL,
+  content jsonb,
+
+  PRIMARY KEY (profile_id, created_at)
+);
+
+ALTER TABLE profile_complaint_report
+  ALTER COLUMN type SET STORAGE external,
+  ALTER COLUMN content SET STORAGE external;
+
+ALTER TABLE profile
+  ADD COLUMN is_muted boolean DEFAULT false,
+  ADD COLUMN auth_backend text NULL;
+
+ALTER TABLE profile
+  ALTER COLUMN auth_backend SET STORAGE external;
+
+UPDATE profile
+   SET auth_backend = 'google'
+ WHERE password = '!';
+
+UPDATE profile
+   SET auth_backend = 'penpot'
+ WHERE password != '!';
+
+-- Table storing a permanent complaint table for register all
+-- permanent bounces and spam reports (complaints) and avoid sending
+-- more emails there.
+CREATE TABLE global_complaint_report (
+  email text NOT NULL,
+  created_at timestamptz NOT NULL DEFAULT now(),
+
+  type text NOT NULL,
+  content jsonb,
+
+  PRIMARY KEY (email, created_at)
+);
+
+ALTER TABLE global_complaint_report
+  ALTER COLUMN type SET STORAGE external,
+  ALTER COLUMN content SET STORAGE external;
diff --git a/backend/src/app/rpc/mutations/profile.clj b/backend/src/app/rpc/mutations/profile.clj
index 3b18c3a36..81ab78746 100644
--- a/backend/src/app/rpc/mutations/profile.clj
+++ b/backend/src/app/rpc/mutations/profile.clj
@@ -55,12 +55,11 @@
 
 (sv/defmethod ::register-profile {:auth false :rlimit :password}
   [{:keys [pool tokens session] :as cfg} {:keys [token] :as params}]
-  (when-not (:registration-enabled cfg/config)
+  (when-not (cfg/get :registration-enabled)
     (ex/raise :type :restriction
               :code :registration-disabled))
 
-  (when-not (email-domain-in-whitelist? (:registration-domain-whitelist cfg/config)
-                                        (:email params))
+  (when-not (email-domain-in-whitelist? (cfg/get :registration-domain-whitelist) (:email params))
     (ex/raise :type :validation
               :code :email-domain-is-not-allowed))
 
@@ -97,20 +96,30 @@
             {:transform-response ((:create session) (:id profile))}))
 
         ;; If no token is provided, send a verification email
-        (let [token (tokens :generate
-                            {:iss :verify-email
-                             :exp (dt/in-future "48h")
-                             :profile-id (:id profile)
-                             :email (:email profile)})]
+        (let [vtoken (tokens :generate
+                             {:iss :verify-email
+                              :exp (dt/in-future "48h")
+                              :profile-id (:id profile)
+                              :email (:email profile)})
+              ptoken (tokens :generate-predefined
+                             {:iss :profile-identity
+                              :profile-id (:id profile)})]
+
+          ;; Don't allow proceed in register page if the email is
+          ;; already reported as permanent bounced
+          (when (emails/has-bounce-reports? conn (:email profile))
+            (ex/raise :type :validation
+                      :code :email-has-permanent-bounces
+                      :hint "looks like the email has one or many bounces reported"))
 
           (emails/send! conn emails/register
                         {:to (:email profile)
                          :name (:fullname profile)
-                         :token token})
+                         :token vtoken
+                         :extra-data ptoken})
 
           profile)))))
 
-
 (defn email-domain-in-whitelist?
   "Returns true if email's domain is in the given whitelist or if given
   whitelist is an empty string."
@@ -155,8 +164,8 @@
 (defn- create-profile
   "Create the profile entry on the database with limited input
   filling all the other fields with defaults."
-  [conn {:keys [id fullname email password demo? props is-active]
-         :or {is-active false}
+  [conn {:keys [id fullname email password demo? props is-active is-muted]
+         :or {is-active false is-muted false}
          :as params}]
   (let [id       (or id (uuid/next))
         demo?    (if (boolean? demo?) demo? false)
@@ -168,9 +177,11 @@
                       {:id id
                        :fullname fullname
                        :email (str/lower email)
+                       :auth-backend "penpot"
                        :password password
                        :props props
                        :is-active active?
+                       :is-muted is-muted
                        :is-demo demo?})
           (update :props db/decode-transit-pgobject))
       (catch org.postgresql.util.PSQLException e
@@ -252,11 +263,12 @@
 
 ;; --- Mutation: Register if not exists
 
+(s/def ::backend ::us/string)
 (s/def ::login-or-register
-  (s/keys :req-un [::email ::fullname]))
+  (s/keys :req-un [::email ::fullname ::backend]))
 
 (sv/defmethod ::login-or-register {:auth false}
-  [{:keys [pool] :as cfg} {:keys [email fullname] :as params}]
+  [{:keys [pool] :as cfg} {:keys [email backend fullname] :as params}]
   (letfn [(populate-additional-data [conn profile]
             (let [data (profile/retrieve-additional-data conn (:id profile))]
               (merge profile data)))
@@ -266,6 +278,7 @@
                         {:id (uuid/next)
                          :fullname fullname
                          :email (str/lower email)
+                         :auth-backend backend
                          :is-active true
                          :password "!"
                          :is-demo false}))
@@ -372,16 +385,30 @@
                           {:iss :change-email
                            :exp (dt/in-future "15m")
                            :profile-id profile-id
-                           :email email})]
+                           :email email})
+          ptoken  (tokens :generate-predefined
+                          {:iss :profile-identity
+                           :profile-id (:id profile)})]
 
       (when (not= email (:email profile))
         (check-profile-existence! conn params))
 
+      (when-not (emails/allow-send-emails? conn profile)
+        (ex/raise :type :validation
+                  :code :profile-is-muted
+                  :hint "looks like the profile has reported repeatedly as spam or has permanent bounces."))
+
+      (when (emails/has-bounce-reports? conn email)
+        (ex/raise :type :validation
+                  :code :email-has-permanent-bounces
+                  :hint "looks like the email you invite has been repeatedly reported as spam or permanent bounce"))
+
       (emails/send! conn emails/change-email
                     {:to (:email profile)
                      :name (:fullname profile)
                      :pending-email email
-                     :token token})
+                     :token token
+                     :extra-data ptoken})
       nil)))
 
 (defn select-profile-for-update
@@ -403,11 +430,15 @@
               (assoc profile :token token)))
 
           (send-email-notification [conn profile]
-            (emails/send! conn emails/password-recovery
-                          {:to (:email profile)
-                           :token (:token profile)
-                           :name (:fullname profile)})
-            nil)]
+            (let [ptoken (tokens :generate-predefined
+                                 {:iss :profile-identity
+                                  :profile-id (:id profile)})]
+              (emails/send! conn emails/password-recovery
+                            {:to (:email profile)
+                             :token (:token profile)
+                             :name (:fullname profile)
+                             :extra-data ptoken})
+              nil))]
 
     (db/with-atomic [conn pool]
       (when-let [profile (profile/retrieve-profile-data-by-email conn email)]
@@ -415,6 +446,17 @@
           (ex/raise :type :validation
                     :code :profile-not-verified
                     :hint "the user need to validate profile before recover password"))
+
+        (when-not (emails/allow-send-emails? conn profile)
+          (ex/raise :type :validation
+                    :code :profile-is-muted
+                    :hint "looks like the profile has reported repeatedly as spam or has permanent bounces."))
+
+        (when (emails/has-bounce-reports? conn (:email profile))
+          (ex/raise :type :validation
+                    :code :email-has-permanent-bounces
+                    :hint "looks like the email you invite has been repeatedly reported as spam or permanent bounce"))
+
         (->> profile
              (create-recovery-token)
              (send-email-notification conn))))))
diff --git a/backend/src/app/rpc/mutations/teams.clj b/backend/src/app/rpc/mutations/teams.clj
index 5abbb4152..c5e37d001 100644
--- a/backend/src/app/rpc/mutations/teams.clj
+++ b/backend/src/app/rpc/mutations/teams.clj
@@ -297,26 +297,48 @@
 (sv/defmethod ::invite-team-member
   [{:keys [pool tokens] :as cfg} {:keys [profile-id team-id email role] :as params}]
   (db/with-atomic [conn pool]
-    (let [perms   (teams/check-edition-permissions! conn profile-id team-id)
-          profile (db/get-by-id conn :profile profile-id)
-          member  (profile/retrieve-profile-data-by-email conn email)
-          team    (db/get-by-id conn :team team-id)
-          token   (tokens :generate
-                          {:iss :team-invitation
-                           :exp (dt/in-future "24h")
-                           :profile-id (:id profile)
-                           :role role
-                           :team-id team-id
-                           :member-email (:email member email)
-                           :member-id (:id member)})]
+    (let [perms    (teams/check-edition-permissions! conn profile-id team-id)
+          profile  (db/get-by-id conn :profile profile-id)
+          member   (profile/retrieve-profile-data-by-email conn email)
+          team     (db/get-by-id conn :team team-id)
+          itoken   (tokens :generate
+                           {:iss :team-invitation
+                            :exp (dt/in-future "24h")
+                            :profile-id (:id profile)
+                            :role role
+                            :team-id team-id
+                            :member-email (:email member email)
+                            :member-id (:id member)})
+          ptoken   (tokens :generate-predefined
+                           {:iss :profile-identity
+                            :profile-id (:id profile)})]
 
       (when-not (some :is-admin perms)
         (ex/raise :type :validation
                   :code :insufficient-permissions))
 
+      ;; First check if the current profile is allowed to send emails.
+      (when-not (emails/allow-send-emails? conn profile)
+        (ex/raise :type :validation
+                  :code :profile-is-muted
+                  :hint "looks like the profile has reported repeatedly as spam or has permanent bounces"))
+
+      (when (and member (not (emails/allow-send-emails? conn member)))
+        (ex/raise :type :validation
+                  :code :member-is-muted
+                  :hint "looks like the profile has reported repeatedly as spam or has permanent bounces"))
+
+      ;; Secondly check if the invited member email is part of the
+      ;; global spam/bounce report.
+      (when (emails/has-bounce-reports? conn email)
+        (ex/raise :type :validation
+                  :code :email-has-permanent-bounces
+                  :hint "looks like the email you invite has been repeatedly reported as spam or permanent bounce"))
+
       (emails/send! conn emails/invite-to-team
                     {:to email
                      :invited-by (:fullname profile)
                      :team (:name team)
-                     :token token})
+                     :token itoken
+                     :extra-data ptoken})
       nil)))
diff --git a/backend/src/app/rpc/mutations/verify_token.clj b/backend/src/app/rpc/mutations/verify_token.clj
index 29eefd5ca..357e20e90 100644
--- a/backend/src/app/rpc/mutations/verify_token.clj
+++ b/backend/src/app/rpc/mutations/verify_token.clj
@@ -90,11 +90,19 @@
     (let [params (merge {:team-id team-id
                          :profile-id member-id}
                         (teams/role->params role))
-          claims (assoc claims :state :created)]
+          claims (assoc claims :state :created)
+          member (profile/retrieve-profile conn member-id)]
 
       (db/insert! conn :team-profile-rel params
                   {:on-conflict-do-nothing true})
 
+      ;; If profile is not yet verified, mark it as verified because
+      ;; accepting an invitation link serves as verification.
+      (when-not (:is-active member)
+        (db/update! conn :profile
+                    {:is-active true}
+                    {:id member-id}))
+
       (if (and (uuid? profile-id)
                (= member-id profile-id))
         ;; If the current session is already matches the invited
diff --git a/backend/src/app/tokens.clj b/backend/src/app/tokens.clj
index a71c49dae..4abbca855 100644
--- a/backend/src/app/tokens.clj
+++ b/backend/src/app/tokens.clj
@@ -60,11 +60,25 @@
 (defmethod ig/pre-init-spec ::tokens [_]
   (s/keys :req-un [::sprops]))
 
+(defn- generate-predefined
+  [cfg {:keys [iss profile-id] :as params}]
+  (case iss
+    :profile-identity
+    (do
+      (us/verify uuid? profile-id)
+      (generate cfg (assoc params
+                           :exp (dt/in-future {:days 30}))))
+
+    (ex/raise :type :internal
+              :code :not-implemented
+              :hint "no predefined token")))
+
 (defmethod ig/init-key ::tokens
   [_ {:keys [sprops] :as cfg}]
   (let [secret (derive-tokens-secret (:secret-key sprops))
         cfg    (assoc cfg ::secret secret)]
     (fn [action params]
       (case action
+        :generate-predefined (generate-predefined cfg params)
         :verify (verify cfg params)
         :generate (generate cfg params)))))
diff --git a/backend/tests/app/tests/helpers.clj b/backend/tests/app/tests/helpers.clj
index 3709b5465..81c442e68 100644
--- a/backend/tests/app/tests/helpers.clj
+++ b/backend/tests/app/tests/helpers.clj
@@ -31,15 +31,24 @@
    [environ.core :refer [env]]
    [expound.alpha :as expound]
    [integrant.core :as ig]
+   [mockery.core :as mk]
    [promesa.core :as p])
   (:import org.postgresql.ds.PGSimpleDataSource))
 
 (def ^:dynamic *system* nil)
 (def ^:dynamic *pool* nil)
 
+(def config
+  (merge {:redis-uri "redis://redis/1"
+          :database-uri "postgresql://postgres/penpot_test"
+          :storage-fs-directory "/tmp/app/storage"
+          :migrations-verbose false}
+         cfg/config))
+
+
 (defn state-init
   [next]
-  (let [config (-> (main/build-system-config cfg/test-config)
+  (let [config (-> (main/build-system-config config)
                    (dissoc :app.srepl/server
                            :app.http/server
                            :app.http/router
@@ -300,3 +309,31 @@
 (defn sleep
   [ms]
   (Thread/sleep ms))
+
+(defn mock-config-get-with
+  "Helper for mock app.config/get"
+  [data]
+  (fn
+    ([key] (get (merge config data) key))
+    ([key default] (get (merge config data) key default))))
+
+(defn create-complaint-for
+  [conn {:keys [id created-at type]}]
+  (db/insert! conn :profile-complaint-report
+              {:profile-id id
+               :created-at (or created-at (dt/now))
+               :type (name type)
+               :content (db/tjson {})}))
+
+(defn create-global-complaint-for
+  [conn {:keys [email type created-at]}]
+  (db/insert! conn :global-complaint-report
+              {:email email
+               :type (name type)
+               :created-at (or created-at (dt/now))
+               :content (db/tjson {})}))
+
+
+(defn reset-mock!
+  [m]
+  (reset! m @(mk/make-mock {})))
diff --git a/backend/tests/app/tests/test_bounces_handling.clj b/backend/tests/app/tests/test_bounces_handling.clj
new file mode 100644
index 000000000..065ada03f
--- /dev/null
+++ b/backend/tests/app/tests/test_bounces_handling.clj
@@ -0,0 +1,316 @@
+;; This Source Code Form is subject to the terms of the Mozilla Public
+;; License, v. 2.0. If a copy of the MPL was not distributed with this
+;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
+;;
+;; This Source Code Form is "Incompatible With Secondary Licenses", as
+;; defined by the Mozilla Public License, v. 2.0.
+;;
+;; Copyright (c) 2021 UXBOX Labs SL
+
+(ns app.tests.test-bounces-handling
+  (:require
+   [clojure.pprint :refer [pprint]]
+   [app.http.awsns :as awsns]
+   [app.emails :as emails]
+   [app.tests.helpers :as th]
+   [app.db :as db]
+   [app.util.time :as dt]
+   [mockery.core :refer [with-mocks]]
+   [clojure.test :as t]))
+
+(t/use-fixtures :once th/state-init)
+(t/use-fixtures :each th/database-reset)
+
+;; (with-mocks [mock {:target 'app.tasks/submit! :return nil}]
+;; Right now we have many different scenarios what can cause a
+;; bounce/complain report.
+
+(defn- decode-row
+  [{:keys [content] :as row}]
+  (cond-> row
+    (db/pgobject? content)
+    (assoc :content (db/decode-transit-pgobject content))))
+
+(defn bounce-report
+  [{:keys [token email] :or {email "user@example.com"}}]
+  {"notificationType" "Bounce",
+   "bounce" {"feedbackId""010701776d7dd251-c08d280d-9f47-41aa-b959-0094fec779d9-000000",
+             "bounceType" "Permanent",
+             "bounceSubType" "General",
+             "bouncedRecipients" [{"emailAddress" email,
+                                   "action" "failed",
+                                   "status" "5.1.1",
+                                   "diagnosticCode" "smtp; 550 5.1.1 user unknown"}]
+             "timestamp" "2021-02-04T14:41:38.000Z",
+             "remoteMtaIp" "22.22.22.22",
+             "reportingMTA" "dsn; b224-13.smtp-out.eu-central-1.amazonses.com"}
+   "mail" {"timestamp" "2021-02-04T14:41:37.020Z",
+           "source" "no-reply@penpot.app",
+           "sourceArn" "arn:aws:ses:eu-central-1:1111111111:identity/penpot.app",
+           "sourceIp" "22.22.22.22",
+           "sendingAccountId" "1111111111",
+           "messageId" "010701776d7dccfc-3c0094e7-01d7-458d-8100-893320186028-000000",
+           "destination" [email],
+           "headersTruncated" false,
+           "headers" [{"name" "Received","value" "from app-pre"},
+                      {"name" "Date","value" "Thu, 4 Feb 2021 14:41:36 +0000 (UTC)"},
+                      {"name" "From","value" "Penpot <no-reply@penpot.app>"},
+                      {"name" "Reply-To","value" "Penpot <no-reply@penpot.app>"},
+                      {"name" "To","value" email},
+                      {"name" "Message-ID","value" "<2054501.5.1612449696846@penpot.app>"},
+                      {"name" "Subject","value" "test"},
+                      {"name" "MIME-Version","value" "1.0"},
+                      {"name" "Content-Type","value" "multipart/mixed;  boundary=\"----=_Part_3_1150363050.1612449696845\""},
+                      {"name" "X-Penpot-Data","value" token}],
+           "commonHeaders" {"from" ["Penpot <no-reply@penpot.app>"],
+                            "replyTo" ["Penpot <no-reply@penpot.app>"],
+                            "date" "Thu, 4 Feb 2021 14:41:36 +0000 (UTC)",
+                            "to" [email],
+                            "messageId" "<2054501.5.1612449696846@penpot.app>",
+                            "subject" "test"}}})
+
+
+(defn complaint-report
+  [{:keys [token email] :or {email "user@example.com"}}]
+  {"notificationType" "Complaint",
+   "complaint" {"feedbackId" "0107017771528618-dcf4d61f-c889-4c8b-a6ff-6f0b6553b837-000000",
+                "complaintSubType" nil,
+                "complainedRecipients" [{"emailAddress" email}],
+                "timestamp" "2021-02-05T08:32:49.000Z",
+                "userAgent" "Yahoo!-Mail-Feedback/2.0",
+                "complaintFeedbackType" "abuse",
+                "arrivalDate" "2021-02-05T08:31:15.000Z"},
+   "mail" {"timestamp" "2021-02-05T08:31:13.715Z",
+           "source" "no-reply@penpot.app",
+           "sourceArn" "arn:aws:ses:eu-central-1:111111111:identity/penpot.app",
+           "sourceIp" "22.22.22.22",
+           "sendingAccountId" "11111111111",
+           "messageId" "0107017771510f33-a0696d28-859c-4f08-9211-8392d1b5c226-000000",
+           "destination" ["user@yahoo.com"],
+           "headersTruncated" false,
+           "headers" [{"name" "Received","value" "from smtp"},
+                      {"name" "Date","value" "Fri, 5 Feb 2021 08:31:13 +0000 (UTC)"},
+                      {"name" "From","value" "Penpot <no-reply@penpot.app>"},
+                      {"name" "Reply-To","value" "Penpot <no-reply@penpot.app>"},
+                      {"name" "To","value" email},
+                      {"name" "Message-ID","value" "<1833063698.279.1612513873536@penpot.app>"},
+                      {"name" "Subject","value" "Verify email."},
+                      {"name" "MIME-Version","value" "1.0"},
+                      {"name" "Content-Type","value" "multipart/mixed;  boundary=\"----=_Part_276_1174403980.1612513873535\""},
+                      {"name" "X-Penpot-Data","value" token}],
+           "commonHeaders" {"from" ["Penpot <no-reply@penpot.app>"],
+                            "replyTo" ["Penpot <no-reply@penpot.app>"],
+                            "date" "Fri, 5 Feb 2021 08:31:13 +0000 (UTC)",
+                            "to" [email],
+                            "messageId" "<1833063698.279.1612513873536@penpot.app>",
+                            "subject" "Verify email."}}})
+
+(t/deftest test-parse-bounce-report
+  (let [profile (th/create-profile* 1)
+        tokens  (:app.tokens/tokens th/*system*)
+        cfg     {:tokens tokens}
+        report  (bounce-report {:token (tokens :generate-predefined
+                                               {:iss :profile-identity
+                                                :profile-id (:id profile)})})
+        result  (#'awsns/parse-notification cfg report)]
+    ;; (pprint result)
+
+    (t/is (= "bounce" (:type result)))
+    (t/is (= "permanent" (:kind result)))
+    (t/is (= "general" (:category result)))
+    (t/is (= ["user@example.com"] (mapv :email (:recipients result))))
+    (t/is (= (:id profile) (:profile-id result)))
+    ))
+
+(t/deftest test-parse-complaint-report
+  (let [profile (th/create-profile* 1)
+        tokens  (:app.tokens/tokens th/*system*)
+        cfg     {:tokens tokens}
+        report  (complaint-report {:token (tokens :generate-predefined
+                                                  {:iss :profile-identity
+                                                   :profile-id (:id profile)})})
+        result  (#'awsns/parse-notification cfg report)]
+    ;; (pprint result)
+    (t/is (= "complaint" (:type result)))
+    (t/is (= "abuse" (:kind result)))
+    (t/is (= nil (:category result)))
+    (t/is (= ["user@example.com"] (into [] (:recipients result))))
+    (t/is (= (:id profile) (:profile-id result)))
+    ))
+
+(t/deftest test-parse-complaint-report-without-token
+  (let [tokens  (:app.tokens/tokens th/*system*)
+        cfg     {:tokens tokens}
+        report  (complaint-report {:token ""})
+        result  (#'awsns/parse-notification cfg report)]
+    (t/is (= "complaint" (:type result)))
+    (t/is (= "abuse" (:kind result)))
+    (t/is (= nil (:category result)))
+    (t/is (= ["user@example.com"] (into [] (:recipients result))))
+    (t/is (= nil (:profile-id result)))
+    ))
+
+(t/deftest test-process-bounce-report
+  (let [profile (th/create-profile* 1)
+        tokens  (:app.tokens/tokens th/*system*)
+        pool    (:app.db/pool th/*system*)
+        cfg     {:tokens tokens :pool pool}
+        report  (bounce-report {:token (tokens :generate-predefined
+                                               {:iss :profile-identity
+                                                :profile-id (:id profile)})})
+        report  (#'awsns/parse-notification cfg report)]
+
+    (#'awsns/process-report cfg report)
+
+    (let [rows (->> (db/query pool :profile-complaint-report {:profile-id (:id profile)})
+                    (mapv decode-row))]
+      (t/is (= 1 (count rows)))
+      (t/is (= "bounce" (get-in rows [0 :type])))
+      (t/is (= "2021-02-04T14:41:38.000Z" (get-in rows [0 :content :timestamp]))))
+
+    (let [rows (->> (db/query pool :global-complaint-report :all)
+                    (mapv decode-row))]
+      (t/is (= 1 (count rows)))
+      (t/is (= "bounce" (get-in rows [0 :type])))
+      (t/is (= "user@example.com" (get-in rows [0 :email]))))
+
+    (let [prof (db/get-by-id pool :profile (:id profile))]
+      (t/is (false? (:is-muted prof))))
+
+    ))
+
+(t/deftest test-process-complaint-report
+  (let [profile (th/create-profile* 1)
+        tokens  (:app.tokens/tokens th/*system*)
+        pool    (:app.db/pool th/*system*)
+        cfg     {:tokens tokens :pool pool}
+        report  (complaint-report {:token (tokens :generate-predefined
+                                                  {:iss :profile-identity
+                                                   :profile-id (:id profile)})})
+        report  (#'awsns/parse-notification cfg report)]
+
+    (#'awsns/process-report cfg report)
+
+    (let [rows (->> (db/query pool :profile-complaint-report {:profile-id (:id profile)})
+                    (mapv decode-row))]
+      (t/is (= 1 (count rows)))
+      (t/is (= "complaint" (get-in rows [0 :type])))
+      (t/is (= "2021-02-05T08:31:15.000Z" (get-in rows [0 :content :timestamp]))))
+
+
+    (let [rows (->> (db/query pool :global-complaint-report :all)
+                    (mapv decode-row))]
+      (t/is (= 1 (count rows)))
+      (t/is (= "complaint" (get-in rows [0 :type])))
+      (t/is (= "user@example.com" (get-in rows [0 :email]))))
+
+
+    (let [prof (db/get-by-id pool :profile (:id profile))]
+      (t/is (false? (:is-muted prof))))
+
+    ))
+
+(t/deftest test-process-bounce-report-to-self
+  (let [profile (th/create-profile* 1)
+        tokens  (:app.tokens/tokens th/*system*)
+        pool    (:app.db/pool th/*system*)
+        cfg     {:tokens tokens :pool pool}
+        report  (bounce-report {:email (:email profile)
+                                   :token (tokens :generate-predefined
+                                                  {:iss :profile-identity
+                                                   :profile-id (:id profile)})})
+        report  (#'awsns/parse-notification cfg report)]
+
+    (#'awsns/process-report cfg report)
+
+    (let [rows (db/query pool :profile-complaint-report {:profile-id (:id profile)})]
+      (t/is (= 1 (count rows))))
+
+    (let [rows (db/query pool :global-complaint-report :all)]
+      (t/is (= 1 (count rows))))
+
+    (let [prof (db/get-by-id pool :profile (:id profile))]
+      (t/is (true? (:is-muted prof))))))
+
+(t/deftest test-process-complaint-report-to-self
+  (let [profile (th/create-profile* 1)
+        tokens  (:app.tokens/tokens th/*system*)
+        pool    (:app.db/pool th/*system*)
+        cfg     {:tokens tokens :pool pool}
+        report  (complaint-report {:email (:email profile)
+                                   :token (tokens :generate-predefined
+                                                  {:iss :profile-identity
+                                                   :profile-id (:id profile)})})
+        report  (#'awsns/parse-notification cfg report)]
+
+    (#'awsns/process-report cfg report)
+
+    (let [rows (db/query pool :profile-complaint-report {:profile-id (:id profile)})]
+      (t/is (= 1 (count rows))))
+
+    (let [rows (db/query pool :global-complaint-report :all)]
+      (t/is (= 1 (count rows))))
+
+    (let [prof (db/get-by-id pool :profile (:id profile))]
+      (t/is (true? (:is-muted prof))))))
+
+(t/deftest test-allow-send-messages-predicate-with-bounces
+  (with-mocks [mock {:target 'app.config/get
+                     :return (th/mock-config-get-with
+                              {:profile-bounce-threshold 3
+                               :profile-complaint-threshold 2})}]
+    (let [profile (th/create-profile* 1)
+          pool    (:app.db/pool th/*system*)]
+      (th/create-complaint-for pool {:type :bounce :id (:id profile) :created-at (dt/in-past {:days 8})})
+      (th/create-complaint-for pool {:type :bounce :id (:id profile)})
+      (th/create-complaint-for pool {:type :bounce :id (:id profile)})
+
+      (t/is (true? (emails/allow-send-emails? pool profile)))
+      (t/is (= 4 (:call-count (deref mock))))
+
+      (th/create-complaint-for pool {:type :bounce :id (:id profile)})
+      (t/is (false? (emails/allow-send-emails? pool profile))))))
+
+
+(t/deftest test-allow-send-messages-predicate-with-complaints
+  (with-mocks [mock {:target 'app.config/get
+                     :return (th/mock-config-get-with
+                              {:profile-bounce-threshold 3
+                               :profile-complaint-threshold 2})}]
+    (let [profile (th/create-profile* 1)
+          pool    (:app.db/pool th/*system*)]
+      (th/create-complaint-for pool {:type :bounce :id (:id profile) :created-at (dt/in-past {:days 8})})
+      (th/create-complaint-for pool {:type :bounce :id (:id profile) :created-at (dt/in-past {:days 8})})
+      (th/create-complaint-for pool {:type :bounce :id (:id profile)})
+      (th/create-complaint-for pool {:type :bounce :id (:id profile)})
+      (th/create-complaint-for pool {:type :complaint :id (:id profile)})
+
+      (t/is (true? (emails/allow-send-emails? pool profile)))
+      (t/is (= 4 (:call-count (deref mock))))
+
+      (th/create-complaint-for pool {:type :complaint :id (:id profile)})
+      (t/is (false? (emails/allow-send-emails? pool profile))))))
+
+(t/deftest test-has-complaint-reports-predicate
+  (let [profile (th/create-profile* 1)
+        pool    (:app.db/pool th/*system*)]
+
+    (t/is (false? (emails/has-complaint-reports? pool (:email profile))))
+
+    (th/create-global-complaint-for pool {:type :bounce :email (:email profile)})
+    (t/is (false? (emails/has-complaint-reports? pool (:email profile))))
+
+    (th/create-global-complaint-for pool {:type :complaint :email (:email profile)})
+    (t/is (true? (emails/has-complaint-reports? pool (:email profile))))))
+
+(t/deftest test-has-bounce-reports-predicate
+  (let [profile (th/create-profile* 1)
+        pool    (:app.db/pool th/*system*)]
+
+    (t/is (false? (emails/has-bounce-reports? pool (:email profile))))
+
+    (th/create-global-complaint-for pool {:type :complaint :email (:email profile)})
+    (t/is (false? (emails/has-bounce-reports? pool (:email profile))))
+
+    (th/create-global-complaint-for pool {:type :bounce :email (:email profile)})
+    (t/is (true? (emails/has-bounce-reports? pool (:email profile))))))
diff --git a/backend/tests/app/tests/test_emails.clj b/backend/tests/app/tests/test_emails.clj
index 1ab3eca1e..7381f510a 100644
--- a/backend/tests/app/tests/test_emails.clj
+++ b/backend/tests/app/tests/test_emails.clj
@@ -11,7 +11,6 @@
   (:require
    [clojure.test :as t]
    [promesa.core :as p]
-   [mockery.core :refer [with-mock]]
    [app.db :as db]
    [app.emails :as emails]
    [app.tests.helpers :as th]))
diff --git a/backend/tests/app/tests/test_services_profile.clj b/backend/tests/app/tests/test_services_profile.clj
index e6c9bf6ab..b355285e5 100644
--- a/backend/tests/app/tests/test_services_profile.clj
+++ b/backend/tests/app/tests/test_services_profile.clj
@@ -18,6 +18,9 @@
    [app.rpc.mutations.profile :as profile]
    [app.tests.helpers :as th]))
 
+;; TODO: profile deletion with teams
+;; TODO: profile deletion with owner teams
+
 (t/use-fixtures :once th/state-init)
 (t/use-fixtures :each th/database-reset)
 
@@ -187,11 +190,6 @@
     (t/testing "not allowed email domain"
       (t/is (false? (profile/email-domain-in-whitelist? whitelist "username@somedomain.com"))))))
 
-;; TODO: profile deletion with teams
-;; TODO: profile deletion with owner teams
-;; TODO: profile registration
-;; TODO: profile password recovery
-
 (t/deftest test-register-when-registration-disabled
   (with-mocks [mock {:target 'app.config/get
                      :return (th/mock-config-get-with
@@ -267,8 +265,7 @@
         (t/is (= (:code edata) :email-has-permanent-bounces))))))
 
 (t/deftest test-register-profile-with-complained-email
-  (with-mocks [mock {:target 'app.emails/send!
-                     :return nil}]
+  (with-mocks [mock {:target 'app.emails/send! :return nil}]
     (let [pool  (:app.db/pool th/*system*)
           data  {::th/type :register-profile
                  :email "user@example.com"
@@ -282,3 +279,86 @@
 
       (let [result (:result out)]
         (t/is (= (:email data) (:email result)))))))
+
+(t/deftest test-email-change-request
+  (with-mocks [mock {:target 'app.emails/send! :return nil}]
+    (let [profile (th/create-profile* 1)
+          pool  (:app.db/pool th/*system*)
+          data  {::th/type :request-email-change
+                 :profile-id (:id profile)
+                 :email "user1@example.com"}]
+
+      ;; without complaints
+      (let [out (th/mutation! data)]
+        ;; (th/print-result! out)
+        (t/is (nil? (:result out)))
+        (let [mock (deref mock)]
+          (t/is (= 1 (:call-count mock)))
+          (t/is (true? (:called? mock)))))
+
+      ;; with complaints
+      (th/create-global-complaint-for pool {:type :complaint :email (:email data)})
+      (let [out (th/mutation! data)]
+        ;; (th/print-result! out)
+        (t/is (nil? (:result out)))
+        (t/is (= 2 (:call-count (deref mock)))))
+
+      ;; with bounces
+      (th/create-global-complaint-for pool {:type :bounce :email (:email data)})
+      (let [out   (th/mutation! data)
+            error (:error out)]
+        ;; (th/print-result! out)
+        (t/is (th/ex-info? error))
+        (t/is (th/ex-of-type? error :validation))
+        (t/is (th/ex-of-code? error :email-has-permanent-bounces))
+        (t/is (= 2 (:call-count (deref mock))))))))
+
+(t/deftest test-request-profile-recovery
+  (with-mocks [mock {:target 'app.emails/send! :return nil}]
+    (let [profile1 (th/create-profile* 1)
+          profile2 (th/create-profile* 2 {:is-active true})
+          pool  (:app.db/pool th/*system*)
+          data  {::th/type :request-profile-recovery}]
+
+      ;; with invalid email
+      (let [data (assoc data :email "foo@bar.com")
+            out  (th/mutation! data)]
+        (t/is (nil? (:result out)))
+        (t/is (= 0 (:call-count (deref mock)))))
+
+      ;; with valid email inactive user
+      (let [data  (assoc data :email (:email profile1))
+            out   (th/mutation! data)
+            error (:error out)]
+        (t/is (= 0 (:call-count (deref mock))))
+        (t/is (th/ex-info? error))
+        (t/is (th/ex-of-type? error :validation))
+        (t/is (th/ex-of-code? error :profile-not-verified)))
+
+      ;; with valid email and active user
+      (let [data  (assoc data :email (:email profile2))
+            out   (th/mutation! data)]
+        ;; (th/print-result! out)
+        (t/is (nil? (:result out)))
+        (t/is (= 1 (:call-count (deref mock)))))
+
+      ;; with valid email and active user with global complaints
+      (th/create-global-complaint-for pool {:type :complaint :email (:email profile2)})
+      (let [data  (assoc data :email (:email profile2))
+            out   (th/mutation! data)]
+        ;; (th/print-result! out)
+        (t/is (nil? (:result out)))
+        (t/is (= 2 (:call-count (deref mock)))))
+
+      ;; with valid email and active user with global bounce
+      (th/create-global-complaint-for pool {:type :bounce :email (:email profile2)})
+      (let [data  (assoc data :email (:email profile2))
+            out   (th/mutation! data)
+            error (:error out)]
+        ;; (th/print-result! out)
+        (t/is (= 2 (:call-count (deref mock))))
+        (t/is (th/ex-info? error))
+        (t/is (th/ex-of-type? error :validation))
+        (t/is (th/ex-of-code? error :email-has-permanent-bounces)))
+
+      )))
diff --git a/backend/tests/app/tests/test_services_teams.clj b/backend/tests/app/tests/test_services_teams.clj
new file mode 100644
index 000000000..da6ddb688
--- /dev/null
+++ b/backend/tests/app/tests/test_services_teams.clj
@@ -0,0 +1,88 @@
+;; This Source Code Form is subject to the terms of the Mozilla Public
+;; License, v. 2.0. If a copy of the MPL was not distributed with this
+;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
+;;
+;; This Source Code Form is "Incompatible With Secondary Licenses", as
+;; defined by the Mozilla Public License, v. 2.0.
+;;
+;; Copyright (c) 2020 UXBOX Labs SL
+
+(ns app.tests.test-services-teams
+  (:require
+   [app.common.uuid :as uuid]
+   [app.db :as db]
+   [app.http :as http]
+   [app.storage :as sto]
+   [app.tests.helpers :as th]
+   [mockery.core :refer [with-mocks]]
+   [clojure.test :as t]
+   [datoteka.core :as fs]))
+
+(t/use-fixtures :once th/state-init)
+(t/use-fixtures :each th/database-reset)
+
+(t/deftest test-invite-team-member
+  (with-mocks [mock {:target 'app.emails/send! :return nil}]
+    (let [profile1 (th/create-profile* 1 {:is-active true})
+          profile2 (th/create-profile* 2 {:is-active true})
+          profile3 (th/create-profile* 3 {:is-active true :is-muted true})
+
+          team     (th/create-team* 1 {:profile-id (:id profile1)})
+
+          pool     (:app.db/pool th/*system*)
+          data     {::th/type :invite-team-member
+                    :team-id (:id team)
+                    :role :editor
+                    :profile-id (:id profile1)}]
+
+      ;; (th/print-result! out)
+
+      ;; invite external user without complaints
+      (let [data (assoc data :email "foo@bar.com")
+            out  (th/mutation! data)]
+        (t/is (nil? (:result out)))
+        (t/is (= 1 (:call-count (deref mock)))))
+
+      ;; invite internal user without complaints
+      (th/reset-mock! mock)
+      (let [data (assoc data :email (:email profile2))
+            out  (th/mutation! data)]
+        (t/is (nil? (:result out)))
+        (t/is (= 1 (:call-count (deref mock)))))
+
+      ;; invite user with complaint
+      (th/create-global-complaint-for pool {:type :complaint :email "foo@bar.com"})
+      (th/reset-mock! mock)
+      (let [data (assoc data :email "foo@bar.com")
+            out  (th/mutation! data)]
+        (t/is (nil? (:result out)))
+        (t/is (= 1 (:call-count (deref mock)))))
+
+      ;; invite user with bounce
+      (th/reset-mock! mock)
+      (th/create-global-complaint-for pool {:type :bounce :email "foo@bar.com"})
+      (let [data  (assoc data :email "foo@bar.com")
+            out   (th/mutation! data)
+            error (:error out)]
+
+        (t/is (th/ex-info? error))
+        (t/is (th/ex-of-type? error :validation))
+        (t/is (th/ex-of-code? error :email-has-permanent-bounces))
+        (t/is (= 0 (:call-count (deref mock)))))
+
+      ;; invite internal user that is muted
+      (th/reset-mock! mock)
+      (let [data  (assoc data :email (:email profile3))
+            out   (th/mutation! data)
+            error (:error out)]
+
+        (t/is (th/ex-info? error))
+        (t/is (th/ex-of-type? error :validation))
+        (t/is (th/ex-of-code? error :member-is-muted))
+        (t/is (= 0 (:call-count (deref mock)))))
+
+      )))
+
+
+
+
diff --git a/docker/devenv/files/nginx.conf b/docker/devenv/files/nginx.conf
index 04c8fd453..a4b13cb73 100644
--- a/docker/devenv/files/nginx.conf
+++ b/docker/devenv/files/nginx.conf
@@ -99,6 +99,10 @@ http {
             proxy_pass http://127.0.0.1:6060/api;
         }
 
+        location /webhooks {
+            proxy_pass http://127.0.0.1:6060/webhooks;
+        }
+
         location /dbg {
             proxy_pass http://127.0.0.1:6060/dbg;
         }
diff --git a/frontend/resources/locales.json b/frontend/resources/locales.json
index 46f51b44e..322c7bd72 100644
--- a/frontend/resources/locales.json
+++ b/frontend/resources/locales.json
@@ -836,6 +836,28 @@
       "es" : "Autenticación con google esta dehabilitada en el servidor"
     }
   },
+
+  "errors.profile-is-muted" : {
+    "translations" : {
+      "en" : "Your profile has emails muted (spam reports or high bounces).",
+      "es" : "Tu perfil tiene los emails silenciados (por reportes de spam o alto indice de rebote)."
+    }
+  },
+
+  "errors.member-is-muted" : {
+    "translations" : {
+      "en" : "The profile you inviting has emails muted (spam reports or high bounces).",
+      "es" : "El perfil que esta invitando tiene los emails silenciados (por reportes de spam o alto indice de rebote)."
+    }
+  },
+
+  "errors.email-has-permanent-bounces" : {
+    "translations" : {
+      "en" : "The email «%s» has many permanent bounce reports.",
+      "es" : "El email «%s» tiene varios reportes de rebote permanente."
+    }
+  },
+
   "errors.auth.unauthorized" : {
     "used-in" : [ "src/app/main/ui/auth/login.cljs:89" ],
     "translations" : {
diff --git a/frontend/src/app/main/ui/auth/recovery_request.cljs b/frontend/src/app/main/ui/auth/recovery_request.cljs
index 19f4daadd..9a0e0db0b 100644
--- a/frontend/src/app/main/ui/auth/recovery_request.cljs
+++ b/frontend/src/app/main/ui/auth/recovery_request.cljs
@@ -18,9 +18,9 @@
    [app.util.dom :as dom]
    [app.util.i18n :as i18n :refer [tr t]]
    [app.util.router :as rt]
+   [beicon.core :as rx]
    [cljs.spec.alpha :as s]
    [cuerdas.core :as str]
-   [beicon.core :as rx]
    [rumext.alpha :as mf]))
 
 (s/def ::email ::us/email)
@@ -28,37 +28,41 @@
 
 (mf/defc recovery-form
   []
-  (let [form (fm/use-form :spec ::recovery-request-form
-                          :initial {})
-
+  (let [form      (fm/use-form :spec ::recovery-request-form :initial {})
         submitted (mf/use-state false)
 
-        on-error
-        (mf/use-callback
-         (fn [{:keys [code] :as error}]
-           (reset! submitted false)
-           (if (= code :profile-not-verified)
-             (rx/of (dm/error (tr "auth.notifications.profile-not-verified")
-                              {:timeout nil}))
-
-             (rx/throw error))))
-
         on-success
         (mf/use-callback
-         (fn []
+         (fn [data]
            (reset! submitted false)
            (st/emit! (dm/info (tr "auth.notifications.recovery-token-sent"))
                      (rt/nav :auth-login))))
 
+        on-error
+        (mf/use-callback
+         (fn [data {:keys [code] :as error}]
+           (reset! submitted false)
+           (case code
+             :profile-not-verified
+             (rx/of (dm/error (tr "auth.notifications.profile-not-verified") {:timeout nil}))
+
+             :profile-is-muted
+             (rx/of (dm/error (tr "errors.profile-is-muted")))
+
+             :email-has-permanent-bounces
+             (rx/of (dm/error (tr "errors.email-has-permanent-bounces" (:email data))))
+
+             (rx/throw error))))
+
         on-submit
         (mf/use-callback
          (fn []
            (reset! submitted true)
-           (->> (with-meta (:clean-data @form)
-                  {:on-success on-success
-                   :on-error on-error})
-                (uda/request-profile-recovery)
-                (st/emit!))))]
+           (let [cdata  (:clean-data @form)
+                 params (with-meta cdata
+                          {:on-success #(on-success cdata %)
+                           :on-error #(on-error cdata %)})]
+             (st/emit! (uda/request-profile-recovery params)))))]
 
     [:& fm/form {:on-submit on-submit
                  :form form}
diff --git a/frontend/src/app/main/ui/auth/register.cljs b/frontend/src/app/main/ui/auth/register.cljs
index 9d76bd2b9..96635d64b 100644
--- a/frontend/src/app/main/ui/auth/register.cljs
+++ b/frontend/src/app/main/ui/auth/register.cljs
@@ -64,13 +64,17 @@
            (reset! submitted? false)
            (case (:code error)
              :registration-disabled
-             (st/emit! (dm/error (tr "errors.registration-disabled")))
+             (rx/of (dm/error (tr "errors.registration-disabled")))
+
+             :email-has-permanent-bounces
+             (let [email (get @form [:data :email])]
+               (rx/of (dm/error (tr "errors.email-has-permanent-bounces" email))))
 
              :email-already-exists
              (swap! form assoc-in [:errors :email]
                     {:message "errors.email-already-exists"})
 
-             (st/emit! (dm/error (tr "errors.unexpected-error"))))))
+             (rx/throw error))))
 
         on-success
         (mf/use-callback
diff --git a/frontend/src/app/main/ui/dashboard/team.cljs b/frontend/src/app/main/ui/dashboard/team.cljs
index 85ea34d9d..2ab438bdd 100644
--- a/frontend/src/app/main/ui/dashboard/team.cljs
+++ b/frontend/src/app/main/ui/dashboard/team.cljs
@@ -97,13 +97,34 @@
          (st/emitf (dm/success "Invitation sent successfully")
                    (modal/hide)))
 
+        on-error
+        (mf/use-callback
+         (mf/deps team)
+         (fn [form {:keys [type code] :as error}]
+           (let [email (get @form [:data :email])]
+             (cond
+               (and (= :validation type)
+                    (= :profile-is-muted code))
+               (dm/error (tr "errors.profile-is-muted"))
+
+               (and (= :validation type)
+                    (= :member-is-muted code))
+               (dm/error (tr "errors.member-is-muted"))
+
+               (and (= :validation type)
+                    (= :email-has-permanent-bounces))
+               (dm/error (tr "errors.email-has-permanent-bounces" email))
+
+               :else
+               (dm/error (tr "errors.generic"))))))
 
         on-submit
         (mf/use-callback
          (mf/deps team)
          (fn [form]
            (let [params (:clean-data @form)
-                 mdata  {:on-success (partial on-success form)}]
+                 mdata  {:on-success (partial on-success form)
+                         :on-error   (partial on-error form)}]
              (st/emit! (dd/invite-team-member (with-meta params mdata))))))]
 
     [:div.modal.dashboard-invite-modal.form-container
diff --git a/frontend/src/app/main/ui/settings/change_email.cljs b/frontend/src/app/main/ui/settings/change_email.cljs
index cdad1a33e..f4c665f77 100644
--- a/frontend/src/app/main/ui/settings/change_email.cljs
+++ b/frontend/src/app/main/ui/settings/change_email.cljs
@@ -40,14 +40,20 @@
   (s/keys :req-un [::email-1 ::email-2]))
 
 (defn- on-error
-  [form error]
-  (cond
-    (= (:code error) :email-already-exists)
+  [form {:keys [code] :as error}]
+  (case code
+    :email-already-exists
     (swap! form (fn [data]
                   (let [error {:message (tr "errors.email-already-exists")}]
                     (assoc-in data [:errors :email-1] error))))
 
-    :else
+    :profile-is-muted
+    (rx/of (dm/error (tr "errors.profile-is-muted")))
+
+    :email-has-permanent-bounces
+    (let [email (get @form [:data email])]
+      (rx/of (dm/error (tr "errors.email-has-permanent-bounces" email))))
+
     (rx/throw error)))
 
 (defn- on-success