]> nmode's Git Repositories - signal-cli/blob - src/main/java/org/asamk/signal/Main.java
Use the old config directory .config/textsecure as fallback
[signal-cli] / src / main / java / org / asamk / signal / Main.java
1 /**
2 * Copyright (C) 2015 AsamK
3 *
4 * This program is free software: you can redistribute it and/or modify
5 * it under the terms of the GNU General Public License as published by
6 * the Free Software Foundation, either version 3 of the License, or
7 * (at your option) any later version.
8 *
9 * This program is distributed in the hope that it will be useful,
10 * but WITHOUT ANY WARRANTY; without even the implied warranty of
11 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 * GNU General Public License for more details.
13 *
14 * You should have received a copy of the GNU General Public License
15 * along with this program. If not, see <http://www.gnu.org/licenses/>.
16 */
17 package org.asamk.signal;
18
19 import net.sourceforge.argparse4j.ArgumentParsers;
20 import net.sourceforge.argparse4j.impl.Arguments;
21 import net.sourceforge.argparse4j.inf.*;
22 import org.apache.commons.io.IOUtils;
23 import org.apache.http.util.TextUtils;
24 import org.asamk.Signal;
25 import org.freedesktop.dbus.DBusConnection;
26 import org.freedesktop.dbus.exceptions.DBusException;
27 import org.freedesktop.dbus.exceptions.DBusExecutionException;
28 import org.whispersystems.signalservice.api.crypto.UntrustedIdentityException;
29 import org.whispersystems.signalservice.api.messages.*;
30 import org.whispersystems.signalservice.api.messages.multidevice.SignalServiceSyncMessage;
31 import org.whispersystems.signalservice.api.push.exceptions.EncapsulatedExceptions;
32 import org.whispersystems.signalservice.api.push.exceptions.NetworkFailureException;
33 import org.whispersystems.signalservice.api.push.exceptions.UnregisteredUserException;
34 import org.whispersystems.signalservice.api.util.PhoneNumberFormatter;
35
36 import java.io.File;
37 import java.io.IOException;
38 import java.security.Security;
39 import java.util.ArrayList;
40 import java.util.List;
41
42 public class Main {
43
44 public static final String SIGNAL_BUSNAME = "org.asamk.Signal";
45 public static final String SIGNAL_OBJECTPATH = "/org/asamk/Signal";
46
47 public static void main(String[] args) {
48 // Workaround for BKS truststore
49 Security.insertProviderAt(new org.spongycastle.jce.provider.BouncyCastleProvider(), 1);
50
51 Namespace ns = parseArgs(args);
52 if (ns == null) {
53 System.exit(1);
54 }
55
56 final String username = ns.getString("username");
57 Manager m;
58 Signal ts;
59 DBusConnection dBusConn = null;
60 try {
61 if (ns.getBoolean("dbus") || ns.getBoolean("dbus_system")) {
62 try {
63 m = null;
64 int busType;
65 if (ns.getBoolean("dbus_system")) {
66 busType = DBusConnection.SYSTEM;
67 } else {
68 busType = DBusConnection.SESSION;
69 }
70 dBusConn = DBusConnection.getConnection(busType);
71 ts = (Signal) dBusConn.getRemoteObject(
72 SIGNAL_BUSNAME, SIGNAL_OBJECTPATH,
73 Signal.class);
74 } catch (DBusException e) {
75 e.printStackTrace();
76 if (dBusConn != null) {
77 dBusConn.disconnect();
78 }
79 System.exit(3);
80 return;
81 }
82 } else {
83 String settingsPath = ns.getString("config");
84 if (TextUtils.isEmpty(settingsPath)) {
85 settingsPath = System.getProperty("user.home") + "/.config/signal";
86 if (!new File(settingsPath).exists()) {
87 String legacySettingsPath = System.getProperty("user.home") + "/.config/textsecure";
88 if (new File(legacySettingsPath).exists()) {
89 settingsPath = legacySettingsPath;
90 }
91 }
92 }
93
94 m = new Manager(username, settingsPath);
95 ts = m;
96 if (m.userExists()) {
97 try {
98 m.load();
99 } catch (Exception e) {
100 System.err.println("Error loading state file \"" + m.getFileName() + "\": " + e.getMessage());
101 System.exit(2);
102 return;
103 }
104 }
105 }
106
107 switch (ns.getString("command")) {
108 case "register":
109 if (dBusConn != null) {
110 System.err.println("register is not yet implementd via dbus");
111 System.exit(1);
112 }
113 if (!m.userHasKeys()) {
114 m.createNewIdentity();
115 }
116 try {
117 m.register(ns.getBoolean("voice"));
118 } catch (IOException e) {
119 System.err.println("Request verify error: " + e.getMessage());
120 System.exit(3);
121 }
122 break;
123 case "verify":
124 if (dBusConn != null) {
125 System.err.println("verify is not yet implementd via dbus");
126 System.exit(1);
127 }
128 if (!m.userHasKeys()) {
129 System.err.println("User has no keys, first call register.");
130 System.exit(1);
131 }
132 if (m.isRegistered()) {
133 System.err.println("User registration is already verified");
134 System.exit(1);
135 }
136 try {
137 m.verifyAccount(ns.getString("verificationCode"));
138 } catch (IOException e) {
139 System.err.println("Verify error: " + e.getMessage());
140 System.exit(3);
141 }
142 break;
143 case "send":
144 if (dBusConn == null && !m.isRegistered()) {
145 System.err.println("User is not registered.");
146 System.exit(1);
147 }
148
149 if (ns.getBoolean("endsession")) {
150 if (ns.getList("recipient") == null) {
151 System.err.println("No recipients given");
152 System.err.println("Aborting sending.");
153 System.exit(1);
154 }
155 try {
156 ts.sendEndSessionMessage(ns.<String>getList("recipient"));
157 } catch (IOException e) {
158 handleIOException(e);
159 } catch (EncapsulatedExceptions e) {
160 handleEncapsulatedExceptions(e);
161 } catch (AssertionError e) {
162 handleAssertionError(e);
163 } catch (DBusExecutionException e) {
164 handleDBusExecutionException(e);
165 }
166 } else {
167 String messageText = ns.getString("message");
168 if (messageText == null) {
169 try {
170 messageText = IOUtils.toString(System.in);
171 } catch (IOException e) {
172 System.err.println("Failed to read message from stdin: " + e.getMessage());
173 System.err.println("Aborting sending.");
174 System.exit(1);
175 }
176 }
177
178 try {
179 List<String> attachments = ns.getList("attachment");
180 if (attachments == null) {
181 attachments = new ArrayList<>();
182 }
183 if (ns.getString("group") != null) {
184 byte[] groupId = decodeGroupId(ns.getString("group"));
185 ts.sendGroupMessage(messageText, attachments, groupId);
186 } else {
187 ts.sendMessage(messageText, attachments, ns.<String>getList("recipient"));
188 }
189 } catch (IOException e) {
190 handleIOException(e);
191 } catch (EncapsulatedExceptions e) {
192 handleEncapsulatedExceptions(e);
193 } catch (AssertionError e) {
194 handleAssertionError(e);
195 } catch (GroupNotFoundException e) {
196 handleGroupNotFoundException(e);
197 } catch (AttachmentInvalidException e) {
198 System.err.println("Failed to add attachment: " + e.getMessage());
199 System.err.println("Aborting sending.");
200 System.exit(1);
201 } catch (DBusExecutionException e) {
202 handleDBusExecutionException(e);
203 }
204 }
205
206 break;
207 case "receive":
208 if (dBusConn != null) {
209 System.err.println("receive is not yet implementd via dbus");
210 System.exit(1);
211 }
212 if (!m.isRegistered()) {
213 System.err.println("User is not registered.");
214 System.exit(1);
215 }
216 int timeout = 5;
217 if (ns.getInt("timeout") != null) {
218 timeout = ns.getInt("timeout");
219 }
220 boolean returnOnTimeout = true;
221 if (timeout < 0) {
222 returnOnTimeout = false;
223 timeout = 3600;
224 }
225 try {
226 m.receiveMessages(timeout, returnOnTimeout, new ReceiveMessageHandler(m));
227 } catch (IOException e) {
228 System.err.println("Error while receiving messages: " + e.getMessage());
229 System.exit(3);
230 } catch (AssertionError e) {
231 handleAssertionError(e);
232 }
233 break;
234 case "quitGroup":
235 if (dBusConn != null) {
236 System.err.println("quitGroup is not yet implementd via dbus");
237 System.exit(1);
238 }
239 if (!m.isRegistered()) {
240 System.err.println("User is not registered.");
241 System.exit(1);
242 }
243
244 try {
245 m.sendQuitGroupMessage(decodeGroupId(ns.getString("group")));
246 } catch (IOException e) {
247 handleIOException(e);
248 } catch (EncapsulatedExceptions e) {
249 handleEncapsulatedExceptions(e);
250 } catch (AssertionError e) {
251 handleAssertionError(e);
252 } catch (GroupNotFoundException e) {
253 handleGroupNotFoundException(e);
254 }
255
256 break;
257 case "updateGroup":
258 if (dBusConn != null) {
259 System.err.println("updateGroup is not yet implementd via dbus");
260 System.exit(1);
261 }
262 if (!m.isRegistered()) {
263 System.err.println("User is not registered.");
264 System.exit(1);
265 }
266
267 try {
268 byte[] groupId = null;
269 if (ns.getString("group") != null) {
270 groupId = decodeGroupId(ns.getString("group"));
271 }
272 byte[] newGroupId = m.sendUpdateGroupMessage(groupId, ns.getString("name"), ns.<String>getList("member"), ns.getString("avatar"));
273 if (groupId == null) {
274 System.out.println("Creating new group \"" + Base64.encodeBytes(newGroupId) + "\" …");
275 }
276 } catch (IOException e) {
277 handleIOException(e);
278 } catch (AttachmentInvalidException e) {
279 System.err.println("Failed to add avatar attachment for group\": " + e.getMessage());
280 System.err.println("Aborting sending.");
281 System.exit(1);
282 } catch (GroupNotFoundException e) {
283 handleGroupNotFoundException(e);
284 } catch (EncapsulatedExceptions e) {
285 handleEncapsulatedExceptions(e);
286 }
287
288 break;
289 case "daemon":
290 if (dBusConn != null) {
291 System.err.println("Stop it.");
292 System.exit(1);
293 }
294 if (!m.isRegistered()) {
295 System.err.println("User is not registered.");
296 System.exit(1);
297 }
298 DBusConnection conn = null;
299 try {
300 try {
301 int busType;
302 if (ns.getBoolean("system")) {
303 busType = DBusConnection.SYSTEM;
304 } else {
305 busType = DBusConnection.SESSION;
306 }
307 conn = DBusConnection.getConnection(busType);
308 conn.exportObject(SIGNAL_OBJECTPATH, m);
309 conn.requestBusName(SIGNAL_BUSNAME);
310 } catch (DBusException e) {
311 e.printStackTrace();
312 System.exit(3);
313 }
314 try {
315 m.receiveMessages(3600, false, new DbusReceiveMessageHandler(m, conn));
316 } catch (IOException e) {
317 System.err.println("Error while receiving messages: " + e.getMessage());
318 System.exit(3);
319 } catch (AssertionError e) {
320 handleAssertionError(e);
321 }
322 } finally {
323 if (conn != null) {
324 conn.disconnect();
325 }
326 }
327
328 break;
329 }
330 System.exit(0);
331 } finally {
332 if (dBusConn != null) {
333 dBusConn.disconnect();
334 }
335 }
336 }
337
338 private static void handleGroupNotFoundException(GroupNotFoundException e) {
339 System.err.println("Failed to send to group: " + e.getMessage());
340 System.err.println("Aborting sending.");
341 System.exit(1);
342 }
343
344 private static void handleDBusExecutionException(DBusExecutionException e) {
345 System.err.println("Cannot connect to dbus: " + e.getMessage());
346 System.err.println("Aborting.");
347 System.exit(1);
348 }
349
350 private static byte[] decodeGroupId(String groupId) {
351 try {
352 return Base64.decode(groupId);
353 } catch (IOException e) {
354 System.err.println("Failed to decode groupId (must be base64) \"" + groupId + "\": " + e.getMessage());
355 System.err.println("Aborting sending.");
356 System.exit(1);
357 return null;
358 }
359 }
360
361 private static Namespace parseArgs(String[] args) {
362 ArgumentParser parser = ArgumentParsers.newArgumentParser("signal-cli")
363 .defaultHelp(true)
364 .description("Commandline interface for Signal.")
365 .version(Manager.PROJECT_NAME + " " + Manager.PROJECT_VERSION);
366
367 parser.addArgument("-v", "--version")
368 .help("Show package version.")
369 .action(Arguments.version());
370 parser.addArgument("--config")
371 .help("Set the path, where to store the config (Default: $HOME/.config/signal-cli).");
372
373 MutuallyExclusiveGroup mut = parser.addMutuallyExclusiveGroup();
374 mut.addArgument("-u", "--username")
375 .help("Specify your phone number, that will be used for verification.");
376 mut.addArgument("--dbus")
377 .help("Make request via user dbus.")
378 .action(Arguments.storeTrue());
379 mut.addArgument("--dbus-system")
380 .help("Make request via system dbus.")
381 .action(Arguments.storeTrue());
382
383 Subparsers subparsers = parser.addSubparsers()
384 .title("subcommands")
385 .dest("command")
386 .description("valid subcommands")
387 .help("additional help");
388
389 Subparser parserRegister = subparsers.addParser("register");
390 parserRegister.addArgument("-v", "--voice")
391 .help("The verification should be done over voice, not sms.")
392 .action(Arguments.storeTrue());
393
394 Subparser parserVerify = subparsers.addParser("verify");
395 parserVerify.addArgument("verificationCode")
396 .help("The verification code you received via sms or voice call.");
397
398 Subparser parserSend = subparsers.addParser("send");
399 parserSend.addArgument("-g", "--group")
400 .help("Specify the recipient group ID.");
401 parserSend.addArgument("recipient")
402 .help("Specify the recipients' phone number.")
403 .nargs("*");
404 parserSend.addArgument("-m", "--message")
405 .help("Specify the message, if missing standard input is used.");
406 parserSend.addArgument("-a", "--attachment")
407 .nargs("*")
408 .help("Add file as attachment");
409 parserSend.addArgument("-e", "--endsession")
410 .help("Clear session state and send end session message.")
411 .action(Arguments.storeTrue());
412
413 Subparser parserLeaveGroup = subparsers.addParser("quitGroup");
414 parserLeaveGroup.addArgument("-g", "--group")
415 .required(true)
416 .help("Specify the recipient group ID.");
417
418 Subparser parserUpdateGroup = subparsers.addParser("updateGroup");
419 parserUpdateGroup.addArgument("-g", "--group")
420 .help("Specify the recipient group ID.");
421 parserUpdateGroup.addArgument("-n", "--name")
422 .help("Specify the new group name.");
423 parserUpdateGroup.addArgument("-a", "--avatar")
424 .help("Specify a new group avatar image file");
425 parserUpdateGroup.addArgument("-m", "--member")
426 .nargs("*")
427 .help("Specify one or more members to add to the group");
428
429 Subparser parserReceive = subparsers.addParser("receive");
430 parserReceive.addArgument("-t", "--timeout")
431 .type(int.class)
432 .help("Number of seconds to wait for new messages (negative values disable timeout)");
433
434 Subparser parserDaemon = subparsers.addParser("daemon");
435 parserDaemon.addArgument("--system")
436 .action(Arguments.storeTrue())
437 .help("Use DBus system bus instead of user bus.");
438
439 try {
440 Namespace ns = parser.parseArgs(args);
441 if (!ns.getBoolean("dbus") && !ns.getBoolean("dbus_system")) {
442 if (ns.getString("username") == null) {
443 parser.printUsage();
444 System.err.println("You need to specify a username (phone number)");
445 System.exit(2);
446 }
447 if (!PhoneNumberFormatter.isValidNumber(ns.getString("username"))) {
448 System.err.println("Invalid username (phone number), make sure you include the country code.");
449 System.exit(2);
450 }
451 }
452 if (ns.getList("recipient") != null && !ns.getList("recipient").isEmpty() && ns.getString("group") != null) {
453 System.err.println("You cannot specify recipients by phone number and groups a the same time");
454 System.exit(2);
455 }
456 return ns;
457 } catch (ArgumentParserException e) {
458 parser.handleError(e);
459 return null;
460 }
461 }
462
463 private static void handleAssertionError(AssertionError e) {
464 System.err.println("Failed to send/receive message (Assertion): " + e.getMessage());
465 System.err.println(e.getStackTrace());
466 System.err.println("If you use an Oracle JRE please check if you have unlimited strength crypto enabled, see README");
467 System.exit(1);
468 }
469
470 private static void handleEncapsulatedExceptions(EncapsulatedExceptions e) {
471 System.err.println("Failed to send (some) messages:");
472 for (NetworkFailureException n : e.getNetworkExceptions()) {
473 System.err.println("Network failure for \"" + n.getE164number() + "\": " + n.getMessage());
474 }
475 for (UnregisteredUserException n : e.getUnregisteredUserExceptions()) {
476 System.err.println("Unregistered user \"" + n.getE164Number() + "\": " + n.getMessage());
477 }
478 for (UntrustedIdentityException n : e.getUntrustedIdentityExceptions()) {
479 System.err.println("Untrusted Identity for \"" + n.getE164Number() + "\": " + n.getMessage());
480 }
481 }
482
483 private static void handleIOException(IOException e) {
484 System.err.println("Failed to send message: " + e.getMessage());
485 }
486
487 private static class ReceiveMessageHandler implements Manager.ReceiveMessageHandler {
488 final Manager m;
489
490 public ReceiveMessageHandler(Manager m) {
491 this.m = m;
492 }
493
494 @Override
495 public void handleMessage(SignalServiceEnvelope envelope, SignalServiceContent content, GroupInfo group) {
496 System.out.println("Envelope from: " + envelope.getSource());
497 System.out.println("Timestamp: " + envelope.getTimestamp());
498
499 if (envelope.isReceipt()) {
500 System.out.println("Got receipt.");
501 } else if (envelope.isSignalMessage() | envelope.isPreKeySignalMessage()) {
502 if (content == null) {
503 System.out.println("Failed to decrypt message.");
504 } else {
505 if (content.getDataMessage().isPresent()) {
506 SignalServiceDataMessage message = content.getDataMessage().get();
507
508 System.out.println("Message timestamp: " + message.getTimestamp());
509
510 if (message.getBody().isPresent()) {
511 System.out.println("Body: " + message.getBody().get());
512 }
513 if (message.getGroupInfo().isPresent()) {
514 SignalServiceGroup groupInfo = message.getGroupInfo().get();
515 System.out.println("Group info:");
516 System.out.println(" Id: " + Base64.encodeBytes(groupInfo.getGroupId()));
517 if (groupInfo.getName().isPresent()) {
518 System.out.println(" Name: " + groupInfo.getName().get());
519 } else if (group != null) {
520 System.out.println(" Name: " + group.name);
521 } else {
522 System.out.println(" Name: <Unknown group>");
523 }
524 System.out.println(" Type: " + groupInfo.getType());
525 if (groupInfo.getMembers().isPresent()) {
526 for (String member : groupInfo.getMembers().get()) {
527 System.out.println(" Member: " + member);
528 }
529 }
530 if (groupInfo.getAvatar().isPresent()) {
531 System.out.println(" Avatar:");
532 printAttachment(groupInfo.getAvatar().get());
533 }
534 }
535 if (message.isEndSession()) {
536 System.out.println("Is end session");
537 }
538
539 if (message.getAttachments().isPresent()) {
540 System.out.println("Attachments: ");
541 for (SignalServiceAttachment attachment : message.getAttachments().get()) {
542 printAttachment(attachment);
543 }
544 }
545 }
546 if (content.getSyncMessage().isPresent()) {
547 SignalServiceSyncMessage syncMessage = content.getSyncMessage().get();
548 System.out.println("Received sync message");
549 }
550 }
551 } else {
552 System.out.println("Unknown message received.");
553 }
554 System.out.println();
555 }
556
557 private void printAttachment(SignalServiceAttachment attachment) {
558 System.out.println("- " + attachment.getContentType() + " (" + (attachment.isPointer() ? "Pointer" : "") + (attachment.isStream() ? "Stream" : "") + ")");
559 if (attachment.isPointer()) {
560 final SignalServiceAttachmentPointer pointer = attachment.asPointer();
561 System.out.println(" Id: " + pointer.getId() + " Key length: " + pointer.getKey().length + (pointer.getRelay().isPresent() ? " Relay: " + pointer.getRelay().get() : ""));
562 System.out.println(" Size: " + (pointer.getSize().isPresent() ? pointer.getSize().get() + " bytes" : "<unavailable>") + (pointer.getPreview().isPresent() ? " (Preview is available: " + pointer.getPreview().get().length + " bytes)" : ""));
563 File file = m.getAttachmentFile(pointer.getId());
564 if (file.exists()) {
565 System.out.println(" Stored plaintext in: " + file);
566 }
567 }
568 }
569 }
570
571 private static class DbusReceiveMessageHandler implements Manager.ReceiveMessageHandler {
572 final Manager m;
573 final DBusConnection conn;
574
575 public DbusReceiveMessageHandler(Manager m, DBusConnection conn) {
576 this.m = m;
577 this.conn = conn;
578 }
579
580 @Override
581 public void handleMessage(SignalServiceEnvelope envelope, SignalServiceContent content, GroupInfo group) {
582 System.out.println("Envelope from: " + envelope.getSource());
583 System.out.println("Timestamp: " + envelope.getTimestamp());
584
585 if (envelope.isReceipt()) {
586 System.out.println("Got receipt.");
587 } else if (envelope.isSignalMessage() | envelope.isPreKeySignalMessage()) {
588 if (content == null) {
589 System.out.println("Failed to decrypt message.");
590 } else {
591 if (content.getDataMessage().isPresent()) {
592 SignalServiceDataMessage message = content.getDataMessage().get();
593
594 System.out.println("Message timestamp: " + message.getTimestamp());
595
596 if (message.getBody().isPresent()) {
597 System.out.println("Body: " + message.getBody().get());
598 }
599
600 if (message.getGroupInfo().isPresent()) {
601 SignalServiceGroup groupInfo = message.getGroupInfo().get();
602 System.out.println("Group info:");
603 System.out.println(" Id: " + Base64.encodeBytes(groupInfo.getGroupId()));
604 if (groupInfo.getName().isPresent()) {
605 System.out.println(" Name: " + groupInfo.getName().get());
606 } else if (group != null) {
607 System.out.println(" Name: " + group.name);
608 } else {
609 System.out.println(" Name: <Unknown group>");
610 }
611 System.out.println(" Type: " + groupInfo.getType());
612 if (groupInfo.getMembers().isPresent()) {
613 for (String member : groupInfo.getMembers().get()) {
614 System.out.println(" Member: " + member);
615 }
616 }
617 if (groupInfo.getAvatar().isPresent()) {
618 System.out.println(" Avatar:");
619 printAttachment(groupInfo.getAvatar().get());
620 }
621 }
622 if (message.isEndSession()) {
623 System.out.println("Is end session");
624 }
625
626 List<String> attachments = new ArrayList<>();
627 if (message.getAttachments().isPresent()) {
628 System.out.println("Attachments: ");
629 for (SignalServiceAttachment attachment : message.getAttachments().get()) {
630 if (attachment.isPointer()) {
631 attachments.add(m.getAttachmentFile(attachment.asPointer().getId()).getAbsolutePath());
632 }
633 printAttachment(attachment);
634 }
635 }
636 if (!message.isEndSession() &&
637 !(message.getGroupInfo().isPresent() && message.getGroupInfo().get().getType() != SignalServiceGroup.Type.DELIVER)) {
638 try {
639 conn.sendSignal(new Signal.MessageReceived(
640 SIGNAL_OBJECTPATH,
641 envelope.getSource(),
642 message.getGroupInfo().isPresent() ? message.getGroupInfo().get().getGroupId() : new byte[0],
643 message.getBody().isPresent() ? message.getBody().get() : "",
644 attachments));
645 } catch (DBusException e) {
646 e.printStackTrace();
647 }
648 }
649 }
650 if (content.getSyncMessage().isPresent()) {
651 SignalServiceSyncMessage syncMessage = content.getSyncMessage().get();
652 System.out.println("Received sync message");
653 }
654 }
655 } else {
656 System.out.println("Unknown message received.");
657 }
658 System.out.println();
659 }
660
661 private void printAttachment(SignalServiceAttachment attachment) {
662 System.out.println("- " + attachment.getContentType() + " (" + (attachment.isPointer() ? "Pointer" : "") + (attachment.isStream() ? "Stream" : "") + ")");
663 if (attachment.isPointer()) {
664 final SignalServiceAttachmentPointer pointer = attachment.asPointer();
665 System.out.println(" Id: " + pointer.getId() + " Key length: " + pointer.getKey().length + (pointer.getRelay().isPresent() ? " Relay: " + pointer.getRelay().get() : ""));
666 System.out.println(" Size: " + (pointer.getSize().isPresent() ? pointer.getSize().get() + " bytes" : "<unavailable>") + (pointer.getPreview().isPresent() ? " (Preview is available: " + pointer.getPreview().get().length + " bytes)" : ""));
667 File file = m.getAttachmentFile(pointer.getId());
668 if (file.exists()) {
669 System.out.println(" Stored plaintext in: " + file);
670 }
671 }
672 }
673 }
674 }