]> nmode's Git Repositories - signal-cli/blob - lib/src/main/java/org/asamk/signal/manager/storage/recipients/RecipientStore.java
16ec9bed5fa8c240590b40c7da093362d43fd1bc
[signal-cli] / lib / src / main / java / org / asamk / signal / manager / storage / recipients / RecipientStore.java
1 package org.asamk.signal.manager.storage.recipients;
2
3 import com.fasterxml.jackson.databind.ObjectMapper;
4
5 import org.asamk.signal.manager.api.Pair;
6 import org.asamk.signal.manager.api.UnregisteredRecipientException;
7 import org.asamk.signal.manager.storage.Utils;
8 import org.asamk.signal.manager.storage.contacts.ContactsStore;
9 import org.asamk.signal.manager.storage.profiles.ProfileStore;
10 import org.signal.libsignal.zkgroup.InvalidInputException;
11 import org.signal.libsignal.zkgroup.profiles.ProfileKey;
12 import org.signal.libsignal.zkgroup.profiles.ProfileKeyCredential;
13 import org.slf4j.Logger;
14 import org.slf4j.LoggerFactory;
15 import org.whispersystems.signalservice.api.push.ACI;
16 import org.whispersystems.signalservice.api.push.ServiceId;
17 import org.whispersystems.signalservice.api.push.SignalServiceAddress;
18 import org.whispersystems.signalservice.api.util.UuidUtil;
19
20 import java.io.ByteArrayInputStream;
21 import java.io.ByteArrayOutputStream;
22 import java.io.File;
23 import java.io.FileInputStream;
24 import java.io.FileNotFoundException;
25 import java.io.FileOutputStream;
26 import java.io.IOException;
27 import java.util.ArrayList;
28 import java.util.Base64;
29 import java.util.Collection;
30 import java.util.HashMap;
31 import java.util.List;
32 import java.util.Map;
33 import java.util.Objects;
34 import java.util.Optional;
35 import java.util.Set;
36 import java.util.UUID;
37 import java.util.function.Supplier;
38 import java.util.stream.Collectors;
39
40 public class RecipientStore implements RecipientResolver, ContactsStore, ProfileStore {
41
42 private final static Logger logger = LoggerFactory.getLogger(RecipientStore.class);
43
44 private final ObjectMapper objectMapper;
45 private final File file;
46 private final RecipientMergeHandler recipientMergeHandler;
47 private final SelfAddressProvider selfAddressProvider;
48
49 private final Map<RecipientId, Recipient> recipients;
50 private final Map<Long, Long> recipientsMerged = new HashMap<>();
51
52 private long lastId;
53 private boolean isBulkUpdating;
54
55 public static RecipientStore load(
56 File file, RecipientMergeHandler recipientMergeHandler, SelfAddressProvider selfAddressProvider
57 ) {
58 final var objectMapper = Utils.createStorageObjectMapper();
59 try (var inputStream = new FileInputStream(file)) {
60 final var storage = objectMapper.readValue(inputStream, Storage.class);
61
62 final var recipientStore = new RecipientStore(objectMapper,
63 file,
64 recipientMergeHandler,
65 selfAddressProvider,
66 new HashMap<>(),
67 storage.lastId);
68 final var recipients = storage.recipients.stream().map(r -> {
69 final var recipientId = new RecipientId(r.id, recipientStore);
70 final var address = new RecipientAddress(Optional.ofNullable(r.uuid).map(UuidUtil::parseOrThrow),
71 Optional.ofNullable(r.number));
72
73 Contact contact = null;
74 if (r.contact != null) {
75 contact = new Contact(r.contact.name,
76 r.contact.color,
77 r.contact.messageExpirationTime,
78 r.contact.blocked,
79 r.contact.archived,
80 r.contact.profileSharingEnabled);
81 }
82
83 ProfileKey profileKey = null;
84 if (r.profileKey != null) {
85 try {
86 profileKey = new ProfileKey(Base64.getDecoder().decode(r.profileKey));
87 } catch (InvalidInputException ignored) {
88 }
89 }
90
91 ProfileKeyCredential profileKeyCredential = null;
92 if (r.profileKeyCredential != null) {
93 try {
94 profileKeyCredential = new ProfileKeyCredential(Base64.getDecoder()
95 .decode(r.profileKeyCredential));
96 } catch (Throwable ignored) {
97 }
98 }
99
100 Profile profile = null;
101 if (r.profile != null) {
102 profile = new Profile(r.profile.lastUpdateTimestamp,
103 r.profile.givenName,
104 r.profile.familyName,
105 r.profile.about,
106 r.profile.aboutEmoji,
107 r.profile.avatarUrlPath,
108 r.profile.paymentAddress == null
109 ? null
110 : Base64.getDecoder().decode(r.profile.paymentAddress),
111 Profile.UnidentifiedAccessMode.valueOfOrUnknown(r.profile.unidentifiedAccessMode),
112 r.profile.capabilities.stream()
113 .map(Profile.Capability::valueOfOrNull)
114 .filter(Objects::nonNull)
115 .collect(Collectors.toSet()));
116 }
117
118 return new Recipient(recipientId, address, contact, profileKey, profileKeyCredential, profile);
119 }).collect(Collectors.toMap(Recipient::getRecipientId, r -> r));
120
121 recipientStore.addRecipients(recipients);
122
123 return recipientStore;
124 } catch (FileNotFoundException e) {
125 logger.trace("Creating new recipient store.");
126 return new RecipientStore(objectMapper,
127 file,
128 recipientMergeHandler,
129 selfAddressProvider,
130 new HashMap<>(),
131 0);
132 } catch (IOException e) {
133 logger.warn("Failed to load recipient store", e);
134 throw new RuntimeException(e);
135 }
136 }
137
138 private RecipientStore(
139 final ObjectMapper objectMapper,
140 final File file,
141 final RecipientMergeHandler recipientMergeHandler,
142 final SelfAddressProvider selfAddressProvider,
143 final Map<RecipientId, Recipient> recipients,
144 final long lastId
145 ) {
146 this.objectMapper = objectMapper;
147 this.file = file;
148 this.recipientMergeHandler = recipientMergeHandler;
149 this.selfAddressProvider = selfAddressProvider;
150 this.recipients = recipients;
151 this.lastId = lastId;
152 }
153
154 public void setBulkUpdating(final boolean bulkUpdating) {
155 isBulkUpdating = bulkUpdating;
156 if (!bulkUpdating) {
157 synchronized (recipients) {
158 saveLocked();
159 }
160 }
161 }
162
163 public RecipientAddress resolveRecipientAddress(RecipientId recipientId) {
164 synchronized (recipients) {
165 return getRecipient(recipientId).getAddress();
166 }
167 }
168
169 public Recipient getRecipient(RecipientId recipientId) {
170 synchronized (recipients) {
171 return recipients.get(recipientId);
172 }
173 }
174
175 public Collection<RecipientId> getRecipientIdsWithEnabledProfileSharing() {
176 synchronized (recipients) {
177 return recipients.values().stream().filter(r -> {
178 final var contact = r.getContact();
179 return contact != null && !contact.isBlocked() && contact.isProfileSharingEnabled();
180 }).map(Recipient::getRecipientId).toList();
181 }
182 }
183
184 @Override
185 public RecipientId resolveRecipient(ServiceId serviceId) {
186 return resolveRecipient(new RecipientAddress(serviceId.uuid()), false, false);
187 }
188
189 @Override
190 public RecipientId resolveRecipient(final long recipientId) {
191 final var recipient = getRecipient(new RecipientId(recipientId, this));
192 return recipient == null ? null : recipient.getRecipientId();
193 }
194
195 @Override
196 public RecipientId resolveRecipient(final String identifier) {
197 return resolveRecipient(Utils.getRecipientAddressFromIdentifier(identifier), false, false);
198 }
199
200 public RecipientId resolveRecipient(
201 final String number, Supplier<ACI> aciSupplier
202 ) throws UnregisteredRecipientException {
203 final Optional<Recipient> byNumber;
204 synchronized (recipients) {
205 byNumber = findByNumberLocked(number);
206 }
207 if (byNumber.isEmpty() || byNumber.get().getAddress().uuid().isEmpty()) {
208 final var aci = aciSupplier.get();
209 if (aci == null) {
210 throw new UnregisteredRecipientException(new RecipientAddress(null, number));
211 }
212
213 return resolveRecipient(new RecipientAddress(aci.uuid(), number), false, false);
214 }
215 return byNumber.get().getRecipientId();
216 }
217
218 public RecipientId resolveRecipient(RecipientAddress address) {
219 return resolveRecipient(address, false, false);
220 }
221
222 @Override
223 public RecipientId resolveRecipient(final SignalServiceAddress address) {
224 return resolveRecipient(new RecipientAddress(address), false, false);
225 }
226
227 public RecipientId resolveSelfRecipientTrusted(RecipientAddress address) {
228 return resolveRecipient(address, true, true);
229 }
230
231 public RecipientId resolveRecipientTrusted(RecipientAddress address) {
232 return resolveRecipient(address, true, false);
233 }
234
235 public RecipientId resolveRecipientTrusted(SignalServiceAddress address) {
236 return resolveRecipient(new RecipientAddress(address), true, false);
237 }
238
239 public List<RecipientId> resolveRecipientsTrusted(List<RecipientAddress> addresses) {
240 final List<RecipientId> recipientIds;
241 final List<Pair<RecipientId, RecipientId>> toBeMerged = new ArrayList<>();
242 synchronized (recipients) {
243 recipientIds = addresses.stream().map(address -> {
244 final var pair = resolveRecipientLocked(address, true, false);
245 if (pair.second().isPresent()) {
246 toBeMerged.add(new Pair<>(pair.first(), pair.second().get()));
247 }
248 return pair.first();
249 }).toList();
250 }
251 for (var pair : toBeMerged) {
252 recipientMergeHandler.mergeRecipients(pair.first(), pair.second());
253 }
254 return recipientIds;
255 }
256
257 @Override
258 public void storeContact(RecipientId recipientId, final Contact contact) {
259 synchronized (recipients) {
260 final var recipient = recipients.get(recipientId);
261 storeRecipientLocked(recipientId, Recipient.newBuilder(recipient).withContact(contact).build());
262 }
263 }
264
265 @Override
266 public Contact getContact(RecipientId recipientId) {
267 final var recipient = getRecipient(recipientId);
268 return recipient == null ? null : recipient.getContact();
269 }
270
271 @Override
272 public List<Pair<RecipientId, Contact>> getContacts() {
273 return recipients.entrySet()
274 .stream()
275 .filter(e -> e.getValue().getContact() != null)
276 .map(e -> new Pair<>(e.getKey(), e.getValue().getContact()))
277 .toList();
278 }
279
280 @Override
281 public void deleteContact(RecipientId recipientId) {
282 synchronized (recipients) {
283 final var recipient = recipients.get(recipientId);
284 storeRecipientLocked(recipientId, Recipient.newBuilder(recipient).withContact(null).build());
285 }
286 }
287
288 public void deleteRecipientData(RecipientId recipientId) {
289 synchronized (recipients) {
290 logger.debug("Deleting recipient data for {}", recipientId);
291 final var recipient = recipients.get(recipientId);
292 recipient.getAddress()
293 .uuid()
294 .ifPresent(uuid -> storeRecipientLocked(recipientId,
295 Recipient.newBuilder()
296 .withRecipientId(recipientId)
297 .withAddress(new RecipientAddress(uuid))
298 .build()));
299 }
300 }
301
302 @Override
303 public Profile getProfile(final RecipientId recipientId) {
304 final var recipient = getRecipient(recipientId);
305 return recipient == null ? null : recipient.getProfile();
306 }
307
308 @Override
309 public ProfileKey getProfileKey(final RecipientId recipientId) {
310 final var recipient = getRecipient(recipientId);
311 return recipient == null ? null : recipient.getProfileKey();
312 }
313
314 @Override
315 public ProfileKeyCredential getProfileKeyCredential(final RecipientId recipientId) {
316 final var recipient = getRecipient(recipientId);
317 return recipient == null ? null : recipient.getProfileKeyCredential();
318 }
319
320 @Override
321 public void storeProfile(RecipientId recipientId, final Profile profile) {
322 synchronized (recipients) {
323 final var recipient = recipients.get(recipientId);
324 storeRecipientLocked(recipientId, Recipient.newBuilder(recipient).withProfile(profile).build());
325 }
326 }
327
328 @Override
329 public void storeProfileKey(RecipientId recipientId, final ProfileKey profileKey) {
330 synchronized (recipients) {
331 final var recipient = recipients.get(recipientId);
332 if (profileKey != null && profileKey.equals(recipient.getProfileKey()) && (
333 recipient.getProfile() == null || (
334 recipient.getProfile().getUnidentifiedAccessMode() != Profile.UnidentifiedAccessMode.UNKNOWN
335 && recipient.getProfile().getUnidentifiedAccessMode()
336 != Profile.UnidentifiedAccessMode.DISABLED
337 )
338 )) {
339 return;
340 }
341
342 final var newRecipient = Recipient.newBuilder(recipient)
343 .withProfileKey(profileKey)
344 .withProfileKeyCredential(null)
345 .withProfile(recipient.getProfile() == null
346 ? null
347 : Profile.newBuilder(recipient.getProfile()).withLastUpdateTimestamp(0).build())
348 .build();
349 storeRecipientLocked(recipientId, newRecipient);
350 }
351 }
352
353 @Override
354 public void storeProfileKeyCredential(RecipientId recipientId, final ProfileKeyCredential profileKeyCredential) {
355 synchronized (recipients) {
356 final var recipient = recipients.get(recipientId);
357 storeRecipientLocked(recipientId,
358 Recipient.newBuilder(recipient).withProfileKeyCredential(profileKeyCredential).build());
359 }
360 }
361
362 public boolean isEmpty() {
363 synchronized (recipients) {
364 return recipients.isEmpty();
365 }
366 }
367
368 private void addRecipients(final Map<RecipientId, Recipient> recipients) {
369 this.recipients.putAll(recipients);
370 }
371
372 /**
373 * @param isHighTrust true, if the number/uuid connection was obtained from a trusted source.
374 * Has no effect, if the address contains only a number or a uuid.
375 */
376 private RecipientId resolveRecipient(RecipientAddress address, boolean isHighTrust, boolean isSelf) {
377 final Pair<RecipientId, Optional<RecipientId>> pair;
378 synchronized (recipients) {
379 pair = resolveRecipientLocked(address, isHighTrust, isSelf);
380 }
381
382 if (pair.second().isPresent()) {
383 recipientMergeHandler.mergeRecipients(pair.first(), pair.second().get());
384 }
385 return pair.first();
386 }
387
388 private Pair<RecipientId, Optional<RecipientId>> resolveRecipientLocked(
389 RecipientAddress address, boolean isHighTrust, boolean isSelf
390 ) {
391 if (isHighTrust && !isSelf) {
392 if (selfAddressProvider.getSelfAddress().matches(address)) {
393 isHighTrust = false;
394 }
395 }
396 final var byNumber = address.number().isEmpty()
397 ? Optional.<Recipient>empty()
398 : findByNumberLocked(address.number().get());
399 final var byUuid = address.uuid().isEmpty()
400 ? Optional.<Recipient>empty()
401 : findByUuidLocked(address.uuid().get());
402
403 if (byNumber.isEmpty() && byUuid.isEmpty()) {
404 logger.debug("Got new recipient, both uuid and number are unknown");
405
406 if (isHighTrust || address.uuid().isEmpty() || address.number().isEmpty()) {
407 return new Pair<>(addNewRecipientLocked(address), Optional.empty());
408 }
409
410 return new Pair<>(addNewRecipientLocked(new RecipientAddress(address.uuid().get())), Optional.empty());
411 }
412
413 if (!isHighTrust || address.uuid().isEmpty() || address.number().isEmpty() || byNumber.equals(byUuid)) {
414 return new Pair<>(byUuid.or(() -> byNumber).map(Recipient::getRecipientId).get(), Optional.empty());
415 }
416
417 if (byNumber.isEmpty()) {
418 logger.debug("Got recipient {} existing with uuid, updating with high trust number",
419 byUuid.get().getRecipientId());
420 updateRecipientAddressLocked(byUuid.get().getRecipientId(), address);
421 return new Pair<>(byUuid.get().getRecipientId(), Optional.empty());
422 }
423
424 final var byNumberRecipient = byNumber.get();
425
426 if (byUuid.isEmpty()) {
427 if (byNumberRecipient.getAddress().uuid().isPresent()) {
428 logger.debug(
429 "Got recipient {} existing with number, but different uuid, so stripping its number and adding new recipient",
430 byNumberRecipient.getRecipientId());
431
432 updateRecipientAddressLocked(byNumberRecipient.getRecipientId(),
433 new RecipientAddress(byNumberRecipient.getAddress().uuid().get()));
434 return new Pair<>(addNewRecipientLocked(address), Optional.empty());
435 }
436
437 logger.debug("Got recipient {} existing with number and no uuid, updating with high trust uuid",
438 byNumberRecipient.getRecipientId());
439 updateRecipientAddressLocked(byNumberRecipient.getRecipientId(), address);
440 return new Pair<>(byNumberRecipient.getRecipientId(), Optional.empty());
441 }
442
443 final var byUuidRecipient = byUuid.get();
444
445 if (byNumberRecipient.getAddress().uuid().isPresent()) {
446 logger.debug(
447 "Got separate recipients for high trust number {} and uuid {}, recipient for number has different uuid, so stripping its number",
448 byNumberRecipient.getRecipientId(),
449 byUuidRecipient.getRecipientId());
450
451 updateRecipientAddressLocked(byNumberRecipient.getRecipientId(),
452 new RecipientAddress(byNumberRecipient.getAddress().uuid().get()));
453 updateRecipientAddressLocked(byUuidRecipient.getRecipientId(), address);
454 return new Pair<>(byUuidRecipient.getRecipientId(), Optional.empty());
455 }
456
457 logger.debug("Got separate recipients for high trust number {} and uuid {}, need to merge them",
458 byNumberRecipient.getRecipientId(),
459 byUuidRecipient.getRecipientId());
460 updateRecipientAddressLocked(byUuidRecipient.getRecipientId(), address);
461 // Create a fixed RecipientId that won't update its id after merge
462 final var toBeMergedRecipientId = new RecipientId(byNumberRecipient.getRecipientId().id(), null);
463 mergeRecipientsLocked(byUuidRecipient.getRecipientId(), toBeMergedRecipientId);
464 return new Pair<>(byUuidRecipient.getRecipientId(), Optional.of(toBeMergedRecipientId));
465 }
466
467 private RecipientId addNewRecipientLocked(final RecipientAddress address) {
468 final var nextRecipientId = nextIdLocked();
469 logger.debug("Adding new recipient {} with address {}", nextRecipientId, address);
470 storeRecipientLocked(nextRecipientId, new Recipient(nextRecipientId, address, null, null, null, null));
471 return nextRecipientId;
472 }
473
474 private void updateRecipientAddressLocked(RecipientId recipientId, final RecipientAddress address) {
475 final var recipient = recipients.get(recipientId);
476 storeRecipientLocked(recipientId, Recipient.newBuilder(recipient).withAddress(address).build());
477 }
478
479 long getActualRecipientId(long recipientId) {
480 while (recipientsMerged.containsKey(recipientId)) {
481 final var newRecipientId = recipientsMerged.get(recipientId);
482 logger.debug("Using {} instead of {}, because recipients have been merged", newRecipientId, recipientId);
483 recipientId = newRecipientId;
484 }
485 return recipientId;
486 }
487
488 private void storeRecipientLocked(final RecipientId recipientId, final Recipient recipient) {
489 final var existingRecipient = recipients.get(recipientId);
490 if (existingRecipient == null || !existingRecipient.equals(recipient)) {
491 recipients.put(recipientId, recipient);
492 saveLocked();
493 }
494 }
495
496 private void mergeRecipientsLocked(RecipientId recipientId, RecipientId toBeMergedRecipientId) {
497 final var recipient = recipients.get(recipientId);
498 final var toBeMergedRecipient = recipients.get(toBeMergedRecipientId);
499 recipients.put(recipientId,
500 new Recipient(recipientId,
501 recipient.getAddress(),
502 recipient.getContact() != null ? recipient.getContact() : toBeMergedRecipient.getContact(),
503 recipient.getProfileKey() != null
504 ? recipient.getProfileKey()
505 : toBeMergedRecipient.getProfileKey(),
506 recipient.getProfileKeyCredential() != null
507 ? recipient.getProfileKeyCredential()
508 : toBeMergedRecipient.getProfileKeyCredential(),
509 recipient.getProfile() != null ? recipient.getProfile() : toBeMergedRecipient.getProfile()));
510 recipients.remove(toBeMergedRecipientId);
511 recipientsMerged.put(toBeMergedRecipientId.id(), recipientId.id());
512 saveLocked();
513 }
514
515 private Optional<Recipient> findByNumberLocked(final String number) {
516 return recipients.entrySet()
517 .stream()
518 .filter(entry -> entry.getValue().getAddress().number().isPresent() && number.equals(entry.getValue()
519 .getAddress()
520 .number()
521 .get()))
522 .findFirst()
523 .map(Map.Entry::getValue);
524 }
525
526 private Optional<Recipient> findByUuidLocked(final UUID uuid) {
527 return recipients.entrySet()
528 .stream()
529 .filter(entry -> entry.getValue().getAddress().uuid().isPresent() && uuid.equals(entry.getValue()
530 .getAddress()
531 .uuid()
532 .get()))
533 .findFirst()
534 .map(Map.Entry::getValue);
535 }
536
537 private RecipientId nextIdLocked() {
538 return new RecipientId(++this.lastId, this);
539 }
540
541 private void saveLocked() {
542 if (isBulkUpdating) {
543 return;
544 }
545 final var base64 = Base64.getEncoder();
546 var storage = new Storage(recipients.entrySet().stream().map(pair -> {
547 final var recipient = pair.getValue();
548 final var recipientContact = recipient.getContact();
549 final var contact = recipientContact == null
550 ? null
551 : new Storage.Recipient.Contact(recipientContact.getName(),
552 recipientContact.getColor(),
553 recipientContact.getMessageExpirationTime(),
554 recipientContact.isBlocked(),
555 recipientContact.isArchived(),
556 recipientContact.isProfileSharingEnabled());
557 final var recipientProfile = recipient.getProfile();
558 final var profile = recipientProfile == null
559 ? null
560 : new Storage.Recipient.Profile(recipientProfile.getLastUpdateTimestamp(),
561 recipientProfile.getGivenName(),
562 recipientProfile.getFamilyName(),
563 recipientProfile.getAbout(),
564 recipientProfile.getAboutEmoji(),
565 recipientProfile.getAvatarUrlPath(),
566 recipientProfile.getPaymentAddress() == null
567 ? null
568 : base64.encodeToString(recipientProfile.getPaymentAddress()),
569 recipientProfile.getUnidentifiedAccessMode().name(),
570 recipientProfile.getCapabilities().stream().map(Enum::name).collect(Collectors.toSet()));
571 return new Storage.Recipient(pair.getKey().id(),
572 recipient.getAddress().number().orElse(null),
573 recipient.getAddress().uuid().map(UUID::toString).orElse(null),
574 recipient.getProfileKey() == null
575 ? null
576 : base64.encodeToString(recipient.getProfileKey().serialize()),
577 recipient.getProfileKeyCredential() == null
578 ? null
579 : base64.encodeToString(recipient.getProfileKeyCredential().serialize()),
580 contact,
581 profile);
582 }).toList(), lastId);
583
584 // Write to memory first to prevent corrupting the file in case of serialization errors
585 try (var inMemoryOutput = new ByteArrayOutputStream()) {
586 objectMapper.writeValue(inMemoryOutput, storage);
587
588 var input = new ByteArrayInputStream(inMemoryOutput.toByteArray());
589 try (var outputStream = new FileOutputStream(file)) {
590 input.transferTo(outputStream);
591 }
592 } catch (Exception e) {
593 logger.error("Error saving recipient store file: {}", e.getMessage());
594 }
595 }
596
597 private record Storage(List<Recipient> recipients, long lastId) {
598
599 private record Recipient(
600 long id,
601 String number,
602 String uuid,
603 String profileKey,
604 String profileKeyCredential,
605 Storage.Recipient.Contact contact,
606 Storage.Recipient.Profile profile
607 ) {
608
609 private record Contact(
610 String name,
611 String color,
612 int messageExpirationTime,
613 boolean blocked,
614 boolean archived,
615 boolean profileSharingEnabled
616 ) {}
617
618 private record Profile(
619 long lastUpdateTimestamp,
620 String givenName,
621 String familyName,
622 String about,
623 String aboutEmoji,
624 String avatarUrlPath,
625 String paymentAddress,
626 String unidentifiedAccessMode,
627 Set<String> capabilities
628 ) {}
629 }
630 }
631
632 public interface RecipientMergeHandler {
633
634 void mergeRecipients(RecipientId recipientId, RecipientId toBeMergedRecipientId);
635 }
636 }