From 9afc49625d8a502d33c6eb740734d94f756601a4 Mon Sep 17 00:00:00 2001 From: joe-prosser Date: Mon, 5 Aug 2024 16:29:23 +0100 Subject: [PATCH] feat(commands): add get email by id (#296) --- CHANGELOG.md | 1 + api/src/lib.rs | 23 ++++++++++++++--- api/src/resources/attachments.rs | 12 +++++++++ api/src/resources/audit.rs | 6 ++--- api/src/resources/comment.rs | 14 +---------- api/src/resources/documents.rs | 6 ++++- api/src/resources/email.rs | 39 +++++++++++++++++++++++++---- cli/src/commands/create/comments.rs | 4 +-- cli/src/commands/create/user.rs | 4 +-- cli/src/commands/get/emails.rs | 31 +++++++++++++++++++++-- cli/src/commands/parse/emls.rs | 10 +++++++- cli/src/commands/parse/msgs.rs | 8 +++++- 12 files changed, 125 insertions(+), 33 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5c6789e9..6e7b3546 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,5 @@ # Unreleased +- Add ability to get email by id - Add ability to upload attachment content for comments # v0.29.0 diff --git a/api/src/lib.rs b/api/src/lib.rs index 4125abad..152b1e38 100644 --- a/api/src/lib.rs +++ b/api/src/lib.rs @@ -22,6 +22,7 @@ use resources::{ SummaryResponse, }, documents::{Document, SyncRawEmailsRequest, SyncRawEmailsResponse}, + email::{Email, GetEmailResponse}, integration::{ GetIntegrationResponse, GetIntegrationsResponse, Integration, NewIntegration, PostIntegrationRequest, PostIntegrationResponse, PutIntegrationRequest, @@ -135,7 +136,7 @@ pub use crate::{ Stream, StreamException, StreamExceptionMetadata, }, user::{ - Email, GlobalPermission, Id as UserId, Identifier as UserIdentifier, + Email as UserEmail, GlobalPermission, Id as UserId, Identifier as UserIdentifier, ModifiedPermissions, NewUser, ProjectPermission, UpdateUser, User, Username, }, }, @@ -242,6 +243,11 @@ pub struct GetCommentQuery { pub include_markup: bool, } +#[derive(Serialize)] +pub struct GetEmailQuery { + pub id: String, +} + impl Client { /// Create a new API client. pub fn new(config: Config) -> Result { @@ -424,7 +430,18 @@ impl Client { CommentsIter::new(self, source_name, page_size, timerange) } - /// Get a page of comments from a source. + /// Get a single of email from a bucket. + pub fn get_email(&self, bucket_name: &BucketFullName, id: EmailId) -> Result> { + let query_params = GetEmailQuery { id: id.0 }; + Ok(self + .get_query::<_, _, GetEmailResponse>( + self.endpoints.get_emails(bucket_name)?, + Some(&query_params), + )? + .emails) + } + + /// Get a page of emails from a bucket. pub fn get_emails_iter_page( &self, bucket_name: &BucketFullName, @@ -1517,7 +1534,7 @@ impl<'a> EmailsIter<'a> { } impl<'a> Iterator for EmailsIter<'a> { - type Item = Result>; + type Item = Result>; fn next(&mut self) -> Option { if self.done { diff --git a/api/src/resources/attachments.rs b/api/src/resources/attachments.rs index 86c3b162..45f28008 100644 --- a/api/src/resources/attachments.rs +++ b/api/src/resources/attachments.rs @@ -1,3 +1,4 @@ +use crate::AttachmentReference; use serde::{Deserialize, Serialize}; #[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq)] @@ -7,3 +8,14 @@ pub struct ContentHash(pub String); pub struct UploadAttachmentResponse { pub content_hash: ContentHash, } + +#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq)] +pub struct AttachmentMetadata { + pub name: String, + pub size: u64, + pub content_type: String, + #[serde(skip_serializing_if = "Option::is_none")] + pub attachment_reference: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub content_hash: Option, +} diff --git a/api/src/resources/audit.rs b/api/src/resources/audit.rs index 99546b7b..c289575e 100644 --- a/api/src/resources/audit.rs +++ b/api/src/resources/audit.rs @@ -1,6 +1,6 @@ use chrono::{DateTime, Utc}; -use crate::{Continuation, DatasetId, DatasetName, Email, ProjectName, UserId, Username}; +use crate::{Continuation, DatasetId, DatasetName, ProjectName, UserEmail, UserId, Username}; use super::{comment::CommentTimestampFilter, project::Id as ProjectId}; use serde::{Deserialize, Serialize}; @@ -45,7 +45,7 @@ pub struct AuditEvent { #[derive(Debug, Clone, Deserialize, Serialize)] pub struct PrintableAuditEvent { - pub actor_email: Email, + pub actor_email: UserEmail, pub actor_tenant_name: AuditTenantName, pub event_type: AuditEventType, pub dataset_names: Vec, @@ -79,7 +79,7 @@ struct AuditTenant { #[derive(Debug, Clone, Deserialize, Serialize)] struct AuditUser { display_name: Username, - email: Email, + email: UserEmail, id: UserId, tenant_id: AuditTenantId, username: Username, diff --git a/api/src/resources/comment.rs b/api/src/resources/comment.rs index 6ef2b7c0..bb612587 100644 --- a/api/src/resources/comment.rs +++ b/api/src/resources/comment.rs @@ -1,5 +1,6 @@ use crate::{ error::{Error, Result}, + resources::attachments::AttachmentMetadata, resources::entity_def::Name as EntityName, resources::label_def::Name as LabelName, resources::label_group::Name as LabelGroupName, @@ -23,8 +24,6 @@ use std::{ str::FromStr, }; -use super::attachments::ContentHash; - #[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq, Hash, PartialOrd, Ord)] pub struct Id(pub String); @@ -343,17 +342,6 @@ pub enum Sentiment { #[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq)] pub struct AttachmentReference(pub String); -#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq)] -pub struct AttachmentMetadata { - pub name: String, - pub size: u64, - pub content_type: String, - #[serde(skip_serializing_if = "Option::is_none")] - pub attachment_reference: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub content_hash: Option, -} - #[derive(Debug, Clone, PartialEq, Default, Eq)] pub struct PropertyMap(HashMap); diff --git a/api/src/resources/documents.rs b/api/src/resources/documents.rs index 868f2389..e15c19f1 100644 --- a/api/src/resources/documents.rs +++ b/api/src/resources/documents.rs @@ -2,7 +2,7 @@ use crate::{CommentId, NewComment, PropertyMap, TransformTag}; use serde::{Deserialize, Serialize, Serializer}; use std::collections::{BTreeMap, HashMap}; -use super::email::AttachmentMetadata; +use super::attachments::AttachmentMetadata; #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] pub struct Document { @@ -100,11 +100,15 @@ From: sender@example.com"# name: "hello.pdf".to_string(), size: 1000, content_type: "pdf".to_string(), + attachment_reference: None, + content_hash: None, }, AttachmentMetadata { name: "world.csv".to_string(), size: 9999, content_type: "csv".to_string(), + attachment_reference: None, + content_hash: None, }, ], body: RawEmailBody::Plain("Hello world".to_string()), diff --git a/api/src/resources/email.rs b/api/src/resources/email.rs index be419187..b0756a0b 100644 --- a/api/src/resources/email.rs +++ b/api/src/resources/email.rs @@ -1,7 +1,14 @@ +use crate::Error; +use std::str::FromStr; + use crate::{ReducibleResponse, SplittableRequest}; use chrono::{DateTime, Utc}; use serde::{Deserialize, Serialize}; +use crate::resources::attachments::AttachmentMetadata; + +pub type Result = std::result::Result; + #[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq, Hash)] pub struct Mailbox(pub String); @@ -11,6 +18,14 @@ pub struct MimeContent(pub String); #[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq, Hash)] pub struct Id(pub String); +impl FromStr for Id { + type Err = Error; + + fn from_str(string: &str) -> Result { + Ok(Self(string.to_owned())) + } +} + #[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq)] pub struct EmailMetadata { #[serde(skip_serializing_if = "Option::is_none")] @@ -26,16 +41,25 @@ pub struct EmailMetadata { #[serde(skip_serializing_if = "Option::is_none")] pub conversation_id: Option, #[serde(skip_serializing_if = "Option::is_none")] + pub conversation_topic: Option, + #[serde(skip_serializing_if = "Option::is_none")] pub is_read: Option, #[serde(skip_serializing_if = "Option::is_none")] pub folder: Option>, + #[serde(skip_serializing_if = "Option::is_none")] + pub received_at: Option, } #[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq)] -pub struct AttachmentMetadata { - pub name: String, - pub size: u64, - pub content_type: String, +pub struct Email { + pub id: Id, + pub mailbox: Mailbox, + pub timestamp: DateTime, + pub mime_content: MimeContent, + pub metadata: Option, + pub attachments: Vec, + pub created_at: Option>, + pub updated_at: Option>, } #[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq)] @@ -80,6 +104,11 @@ pub struct Continuation(pub String); #[derive(Debug, Clone, Deserialize)] pub struct EmailsIterPage { - pub emails: Vec, + pub emails: Vec, pub continuation: Option, } + +#[derive(Debug, Clone, Deserialize)] +pub struct GetEmailResponse { + pub emails: Vec, +} diff --git a/cli/src/commands/create/comments.rs b/cli/src/commands/create/comments.rs index 1ec44893..9431123d 100644 --- a/cli/src/commands/create/comments.rs +++ b/cli/src/commands/create/comments.rs @@ -11,8 +11,8 @@ use anyhow::{anyhow, ensure, Context, Result}; use colored::Colorize; use log::{debug, info}; use reinfer_client::{ - resources::comment::AttachmentMetadata, Client, CommentId, DatasetFullName, DatasetIdentifier, - NewAnnotatedComment, NewComment, Source, SourceId, SourceIdentifier, + resources::attachments::AttachmentMetadata, Client, CommentId, DatasetFullName, + DatasetIdentifier, NewAnnotatedComment, NewComment, Source, SourceId, SourceIdentifier, }; use scoped_threadpool::Pool; use std::{ diff --git a/cli/src/commands/create/user.rs b/cli/src/commands/create/user.rs index 951030eb..226dc7d4 100644 --- a/cli/src/commands/create/user.rs +++ b/cli/src/commands/create/user.rs @@ -1,6 +1,6 @@ use anyhow::{Context, Result}; use reinfer_client::{ - Client, Email, GlobalPermission, NewUser, ProjectName, ProjectPermission, Username, + Client, GlobalPermission, NewUser, ProjectName, ProjectPermission, UserEmail, Username, }; use std::collections::hash_map::HashMap; use structopt::StructOpt; @@ -15,7 +15,7 @@ pub struct CreateUserArgs { #[structopt(name = "email")] /// Email address of the new user - email: Email, + email: UserEmail, #[structopt(long = "global-permissions")] /// Global permissions to give to the new user diff --git a/cli/src/commands/get/emails.rs b/cli/src/commands/get/emails.rs index 4a9cea2b..846d32b4 100644 --- a/cli/src/commands/get/emails.rs +++ b/cli/src/commands/get/emails.rs @@ -1,7 +1,7 @@ use anyhow::{Context, Result}; use colored::Colorize; -use reinfer_client::{resources::bucket_statistics::Count, BucketIdentifier, Client}; +use reinfer_client::{resources::bucket_statistics::Count, BucketIdentifier, Client, EmailId}; use std::{ fs::File, io::{self, BufWriter, Write}, @@ -27,10 +27,14 @@ pub struct GetManyEmailsArgs { #[structopt(short = "f", long = "file", parse(from_os_str))] /// Path where to write comments as JSON. If not specified, stdout will be used. path: Option, + + #[structopt(name = "id")] + /// Id of specific email to return + id: Option, } pub fn get_many(client: &Client, args: &GetManyEmailsArgs) -> Result<()> { - let GetManyEmailsArgs { bucket, path } = args; + let GetManyEmailsArgs { bucket, path, id } = args; let file = match path { Some(path) => Some( @@ -41,6 +45,14 @@ pub fn get_many(client: &Client, args: &GetManyEmailsArgs) -> Result<()> { None => None, }; + if let Some(id) = id { + if let Some(file) = file { + return download_email(client, bucket.clone(), id.clone(), file); + } else { + return download_email(client, bucket.clone(), id.clone(), io::stdout().lock()); + } + } + if let Some(file) = file { download_emails(client, bucket.clone(), file) } else { @@ -48,6 +60,21 @@ pub fn get_many(client: &Client, args: &GetManyEmailsArgs) -> Result<()> { } } +fn download_email( + client: &Client, + bucket_identifier: BucketIdentifier, + id: EmailId, + mut writer: impl Write, +) -> Result<()> { + let bucket = client + .get_bucket(bucket_identifier) + .context("Operation to get bucket has failed.")?; + + let response = client.get_email(&bucket.full_name(), id)?; + + print_resources_as_json(response, &mut writer) +} + fn download_emails( client: &Client, bucket_identifier: BucketIdentifier, diff --git a/cli/src/commands/parse/emls.rs b/cli/src/commands/parse/emls.rs index e18d2a57..90785abe 100644 --- a/cli/src/commands/parse/emls.rs +++ b/cli/src/commands/parse/emls.rs @@ -13,7 +13,9 @@ use crate::commands::{ ensure_uip_user_consents_to_ai_unit_charge, parse::{get_files_in_directory, get_progress_bar, Statistics}, }; -use reinfer_client::{resources::email::AttachmentMetadata, BucketIdentifier, Client, NewEmail}; +use reinfer_client::{ + resources::attachments::AttachmentMetadata, BucketIdentifier, Client, NewEmail, +}; use structopt::StructOpt; use super::upload_batch_of_new_emails; @@ -174,6 +176,8 @@ fn read_eml_to_new_email(path: &PathBuf) -> Result { name: attachment_filename.to_owned(), size, content_type: format!(".{}", extension.to_string_lossy()), + attachment_reference: None, + content_hash: None, }); } } @@ -222,11 +226,15 @@ mod tests { name: "hello.txt".to_string(), size: 176, content_type: ".txt".to_string(), + attachment_reference: None, + content_hash: None, }, AttachmentMetadata { name: "world.pdf".to_string(), size: 7476, content_type: ".pdf".to_string(), + attachment_reference: None, + content_hash: None, }, ]; let expected_mime_content = include_str!("../../../tests/samples/test.eml"); diff --git a/cli/src/commands/parse/msgs.rs b/cli/src/commands/parse/msgs.rs index 19f598b9..7dc45181 100644 --- a/cli/src/commands/parse/msgs.rs +++ b/cli/src/commands/parse/msgs.rs @@ -12,8 +12,8 @@ use std::{io::Read, sync::Arc}; use reinfer_client::{ resources::{ + attachments::AttachmentMetadata, documents::{Document, RawEmail, RawEmailBody, RawEmailHeaders}, - email::AttachmentMetadata, }, Client, PropertyMap, SourceIdentifier, TransformTag, }; @@ -147,6 +147,8 @@ fn read_attachment( name, content_type, size: data.len() as u64, + attachment_reference: None, + content_hash: None, }) } @@ -337,11 +339,15 @@ mod tests { name: "hello.txt".to_string(), size: 12, content_type: ".txt".to_string(), + attachment_reference: None, + content_hash: None, }, AttachmentMetadata { name: "world.pdf".to_string(), size: 7302, content_type: ".pdf".to_string(), + attachment_reference: None, + content_hash: None, }, ];