/ backend / src / services / google_api_service.rs
google_api_service.rs
  1  use crate::util::config::GoogleEmailConfig;
  2  use base64::engine::general_purpose::STANDARD;
  3  use base64::Engine;
  4  use chrono::{Duration, Utc};
  5  use jwt::{PKeyWithDigest, SignWithKey};
  6  use openssl::hash::MessageDigest;
  7  use openssl::pkey::PKey;
  8  use serde::{Deserialize, Serialize};
  9  use std::collections::BTreeMap;
 10  
 11  #[derive(Debug, thiserror::Error)]
 12  pub enum GoogleApiError {
 13      #[error("OpenSSL error stack")]
 14      OpenSSLError(#[from] openssl::error::ErrorStack),
 15      #[error("JWT error")]
 16      JwtError(#[from] jwt::Error),
 17      #[error("Reqwest error `{0}`")]
 18      ReqwestError(#[from] reqwest::Error),
 19      #[error("Google api error")]
 20      GoogleApiError,
 21  }
 22  
 23  const GRANT_TYPE_SERVICE_ACCOUNT: &str = "urn:ietf:params:oauth:grant-type:jwt-bearer";
 24  #[derive(Serialize, Deserialize)]
 25  struct GoogleAuthRequest {
 26      grant_type: String,
 27      assertion: String,
 28  }
 29  
 30  impl GoogleAuthRequest {
 31      fn new(assertion: String) -> Self {
 32          Self {
 33              grant_type: GRANT_TYPE_SERVICE_ACCOUNT.to_string(),
 34              assertion,
 35          }
 36      }
 37  }
 38  
 39  const GMAIL_SEND_EMAIL_SCOPE: &str = "https://www.googleapis.com/auth/gmail.send";
 40  
 41  const GOOGLE_AUD_VALUE: &str = "https://oauth2.googleapis.com/token";
 42  
 43  fn create_jwt(config: &GoogleEmailConfig) -> Result<String, GoogleApiError> {
 44      let private_key = PKey::private_key_from_pem(config.service_account.private_key.as_bytes())?;
 45      let key_with_digest = PKeyWithDigest {
 46          digest: MessageDigest::sha256(),
 47          key: private_key,
 48      };
 49  
 50      let mut claims: BTreeMap<&str, &str> = BTreeMap::new();
 51  
 52      claims.insert("iss", &config.service_account.client_email);
 53      claims.insert("scope", GMAIL_SEND_EMAIL_SCOPE);
 54      claims.insert("aud", GOOGLE_AUD_VALUE);
 55  
 56      let now = Utc::now();
 57      let now_timestamp = now.timestamp().to_string();
 58      claims.insert("iat", &now_timestamp);
 59  
 60      let exp_time = now + Duration::hours(1);
 61      let exp_time_timestamp = exp_time.timestamp().to_string();
 62      claims.insert("exp", &exp_time_timestamp);
 63      claims.insert("sub", &config.send_from_email_address);
 64  
 65      Ok(claims.sign_with_key(&key_with_digest)?)
 66  }
 67  
 68  #[derive(Serialize, Deserialize)]
 69  struct GoogleAuthResponse {
 70      access_token: String,
 71      expires_in: u32,
 72      token_type: String,
 73  }
 74  
 75  pub async fn retrieve_token(config: &GoogleEmailConfig) -> Result<String, GoogleApiError> {
 76      let jwt = create_jwt(config)?;
 77  
 78      let client = reqwest::Client::new();
 79  
 80      // GoogleAuthResponse
 81      let response_text = client
 82          .post(&config.service_account.token_uri)
 83          .form(&GoogleAuthRequest::new(jwt))
 84          .send()
 85          .await?
 86          .text()
 87          .await?;
 88  
 89      let response: GoogleAuthResponse = serde_json::from_str(&response_text).map_err(|_| {
 90          println!(
 91              "GOOGLE API ERR: Failed to retrieve token, err: {}",
 92              response_text
 93          );
 94          GoogleApiError::GoogleApiError
 95      })?;
 96  
 97      Ok(response.access_token)
 98  }
 99  
100  #[derive(Serialize, Deserialize)]
101  struct GoogleSendEmailRequest {
102      raw: String,
103  }
104  
105  impl GoogleSendEmailRequest {
106      fn new(from: &str, to: &str, subject: &str, content: &str) -> Self {
107          let raw_message = STANDARD.encode(format!(
108              "From: {}\r\nTo: {}\r\nSubject: {}\r\n\r\n{}\r\n",
109              from, to, subject, content
110          ));
111          Self { raw: raw_message }
112      }
113  }
114  
115  // Note: the `/me/` is a parameter for the CLIENT_ID
116  const SEND_EMAIL_ENDPOINT: &str = "https://gmail.googleapis.com/gmail/v1/users/me/messages/send";
117  
118  #[derive(Serialize, Deserialize)]
119  #[allow(dead_code)]
120  #[serde(rename_all = "camelCase")]
121  struct GoogleSendEmailResponse {
122      id: String,
123      thread_id: String,
124      label_ids: Vec<String>,
125  }
126  
127  pub async fn send_email(
128      receiver_email: &str,
129      subject: &str,
130      content: &str,
131      token: &str,
132      config: &GoogleEmailConfig,
133  ) -> Result<(), GoogleApiError> {
134      let send_email_request = GoogleSendEmailRequest::new(
135          &config.send_from_email_address,
136          receiver_email,
137          subject,
138          content,
139      );
140  
141      let client = reqwest::Client::new();
142      let response_text = client
143          .post(SEND_EMAIL_ENDPOINT)
144          .query(&[("alt", "json"), ("prettyPrint", "false")])
145          .header(reqwest::header::AUTHORIZATION, format!("Bearer {}", token))
146          .json(&send_email_request)
147          .send()
148          .await?
149          .text()
150          .await?;
151  
152      let _response: GoogleSendEmailResponse =
153          serde_json::from_str(&response_text).map_err(|_| {
154              println!(
155                  "GOOGLE API ERR: Failed to send email, err: {}",
156                  response_text
157              );
158              GoogleApiError::GoogleApiError
159          })?;
160  
161      Ok(())
162  }