instructure/canvas-lms · error · ImportError

An email did not pass validation

Error message

An email did not pass validation (#{user_row.email}, error: #{cc.errors.full_messages.join(", ")})

What it means

SisImports::ImportError raised in SIS::UserImporter#process_batch when the CommunicationChannel created for the user's email address fails validation (cc.save! path fails / cc invalid). All of cc.errors.full_messages are joined into the message together with the offending email value.

Solutions

  1. Fix the email value reported in the error message so it is a syntactically valid address
  2. Trim whitespace and remove illegal characters (spaces, multiple @, unescaped commas) from the email column
  3. If email is optional for this user, remove the value rather than sending an invalid one (subject to account policy)
  4. Add CSV-level email format pre-validation before upload

Example fix

// before (users.csv)
user_id,email
1001,jane doe@school edu

// after
user_id,email
1001,jane.doe@school.edu
Defensive patterns

Strategy: validation

Validate before calling

EMAIL_RE = URI::MailTo::EMAIL_REGEXP
rows.each do |r|
  next if r[:email].blank?
  raise "invalid email #{r[:email]}" unless EMAIL_RE.match?(r[:email].strip)
end

Type guard

def valid_email?(v) = v.is_a?(String) && URI::MailTo::EMAIL_REGEXP.match?(v.strip)

Try / catch

begin
  importer.add_user(row)
rescue SisImports::ImportError => e
  raise unless e.message.start_with?('An email did not pass validation')
  logger.warn("Skipping row with bad email: #{e.message}")
end

Prevention

When it happens

Trigger: During process_batch (called from add_user), the row supplies an email column and the importer builds a CommunicationChannel; when the channel is invalid (e.g. malformed or blank email address per EmailValidator / format rules), the else branch raises ImportError with the email and the full error list.

Common situations: Emails with stray spaces, commas, or missing TLDs in users.csv; empty email strings for users where email is required; case or plus-addressing variants tripping stricter validators; data migrated from another SIS with loosely validated addresses.

Understand the failure class

Background: "Invalid ... format", "must be in format X", "does not look like a ..." — invalid argument format errors across CLI tools and libraries — this error's family across 17 libraries.

Related errors


AI-assisted analysis of instructure/canvas-lms@1c9f0bb801 (2026-09-15). Data as JSON: /api/errors/79f707bd9e09c84a. Report an issue: GitHub.

Appendix: source

Thrown at lib/sis/user_importer.rb:431

              sis_cc = nil
            end
            cc = sis_cc || other_cc || user.communication_channels.new
            cc.user_id = user.id
            cc.pseudonym_id = pseudo.id
            cc.path = user_row.email
            cc.bounce_count = 0 if cc.path_changed?
            cc.workflow_state = (status == "deleted") ? "retired" : "active"
            newly_active = cc.path_changed? || (cc.active? && cc.workflow_state_changed?)
            if cc.changed?
              if cc.valid? && cc.save_without_broadcasting
                record_sync_for(user)
                cc_data = SisBatchRollBackData.build_data(sis_batch: @batch, context: cc)
                @roll_back_data << cc_data if cc_data
              else
                msg = "An email did not pass validation "
                msg += "(" + "#{user_row.email}, error: "
                msg += cc.errors.full_messages.join(", ") + ")"
                raise ImportError, msg
              end
              user.touch unless user_touched
              user.clear_email_cache!
            end
            pseudo.sis_communication_channel_id = pseudo.communication_channel_id = cc.id

            if newly_active && @root_account.feature_enabled?(:self_service_user_merge)
              user_ids = ccs.map(&:user_id)
              pseudo_scope = Pseudonym.active.where(user_id: user_ids).group(:user_id)
              active_pseudo_counts = pseudo_scope.count
              sis_pseudo_counts = pseudo_scope.where("account_id = ? AND sis_user_id IS NOT NULL", @root_account).count

              other_ccs = ccs.reject do |other|
                cc_user_id = other.user_id
                same_user = cc_user_id == user.id
                no_active_pseudos = active_pseudo_counts.fetch(cc_user_id, 0) == 0
                active_sis_pseudos = sis_pseudo_counts.fetch(cc_user_id, 0) != 0

View on GitHub (pinned to 1c9f0bb801)