Below is what I believe the proper way of doing this. Note that, since you're not in the context of a live session, some Sitecore mechanisms (such as cross-cluster session migration) aren't available to you. Hence, you must guard against edge cases, particularly when the contact with given identifier already exists and is currently locked by another process.
Since the contact you're creating doesn't exist in xDB yet, there's no need for an actual merge. What you need to do is get the existing contact and update its fields.
public void EnsureUser(string email)
LeaseOwner leaseOwner = new LeaseOwner("YOUR_WORKER_NAME", LeaseOwnerType.OutOfRequestWorker);
ContactRepositoryBase contactRepository = Factory.CreateObject("contactRepository", true) as ContactRepositoryBase;
// Attempt to obtain an exclusive lock on an existing contact in xDB.
LockAttemptResult<Contact> lockResult = contactRepository.TryLoadContact(email, leaseOwner, TimeSpan.FromMinutes(1));
Contact contact = null;
if (lockResult.Status == LockAttemptStatus.AlreadyLocked)
// Another worker or a live web session has an exclusive lock on the contact.
// You can't use this contact right now. It's up to you what to do in this case.
/* ... */
else if (lockResult.Status == LockAttemptStatus.DatabaseUnavailable)
// Database is down. Try to handle this gracefully.
/* ... */
else if (lockResult.Status == LockAttemptStatus.NotFound)
// A contact with the given identifier doesn't exist.
// Just create a new contact object.
contact = contactRepository.CreateContact(Guid.NewGuid());
// Identify it.
contact.Identifiers.Identifier = email;
// And make it known.
contact.Identifiers.IdentificationLevel = Sitecore.Analytics.Model.ContactIdentificationLevel.Known;
// We successfull locked an existing contact.
contact = lockResult.Object;
// Set some other data on the contact:
/* ... */
// Save the contact and release the lock.
if (contact != null)
var options = new ContactSaveOptions(release: true, owner: leaseOwner);
Note that instead of the Contact Manager, I used the Contact Repository in the above code, since it works directly with the xDB storage. The Contact Manager in your sample code queries and saves contacts in the Shared Session first, and only then accesses xDB via the underlying Contact Repository.